Всем привет. Сегодня будет заключительная статья на тему программирования в мире 3D, как вводная во все возможные темы.
В 3D помимо статических обьектов и поверхностей, по которым можно ходить хотябы камерой. Нужны еще анимированные модели - такие как персонажи, еффекты, детали, которыми можно описывать какие-то наблюдаемые явления, частицы. Всё это можно создавать в программном инструменте Blender, Maya и тд.
Хочу продемонстрировать простенький подход минимального болванчика.
Для этого ничего не потребуется, только Blender (пользуюсь базовой настройкой версии 4.2.3 LTS), нужно правда для теста меша проверить его на любом скелете из миксамо.
Скрытый текст
#import bpy
#from math import *
#from mathutils import *
#vs = [(0,0,0)]
#t = 7
#es = []
#fs = []
#for i in range(t):
# p=2.0*3.1415*-i/t;
# for j in range(i):
# p1=2.0*3.1415*-j/5;
# vs.append((cos(p1),sin(p1),cos(p1)))
#mesh = bpy.data.meshes.new('mesh')
#mesh.from_pydata(vs,es,fs)
#mesh.update()
##meshobj
#meshobj= bpy.data.objects.new('mesh',mesh)
##collection
#new_collection = bpy.data.collections.new('new_collection')
#bpy.context.scene.collection.children.link(new_collection)
#new_collection.objects.link(meshobj)
import bpy
from math import *
from mathutils import *
vs = []
#2*139
t = 2
es = []
fs = []
for i in range(t):
#0
vs.append(( i*2/3.5, i*2/3.5,i*7.4))
vs.append(( 0, i*2/3.5,i*7.4))
vs.append(( i*2/3.5,-i*2/3.5,i*7.4))
vs.append((0,-i*2/3.5, i*7.4))
vs.append((i*2/3.5,i*2/3.5,i*6.3))
vs.append((0,i*2/3.5,i*6.3))
vs.append((i*2/3.5,-i*2/3.5,i*6.3))
vs.append((0,-i*2/3.5,i*6.3))
vs.append((i*2/6.5,i*2/6.5,i*6.3))
vs.append((0,i*2/6.5,i*6.3))
vs.append((i*2/6.5,-i*2/6.5,i*6.3))
vs.append((0,-i*2/6.5,i*6.3))
vs.append((i*2/6.5,i*2/6.5,i*6.2))
vs.append((0,i*2/6.5,i*6.2))
vs.append((i*2/6.5,-i*2/6.5,i*6.2))
vs.append((0,-i*2/6.5,i*6.2))
vs.append((i*2/3,i*2/6.5,i*6.2))
vs.append((0,i*2/6.5,i*6.2))
vs.append((i*2/3,-i*2/6.5,i*6.2))
vs.append((0,-i*2/6.5,i*6.2))
vs.append((i*2/3,i*2/6.5,i*5.8))
vs.append((0,i*2/6.5,i*5.8))
vs.append((i*2/3,-i*2/6.5,i*5.8))
vs.append((0,-i*2/6.5,i*5.8))
vs.append((i*2/1.9,i*2/6.5,i*5.8))
vs.append((0,i*2/6.5,i*5.8))
vs.append((i*2/1.9,-i*2/6.5,i*5.8))
vs.append((0,-i*2/6.5,i*5.8))
vs.append((i*2/1.9,i*2/6.5,i*5.0))
vs.append((0,i*2/6.5,i*5.0))
vs.append((i*2/1.9,-i*2/6.5,i*5.0))
vs.append((0,-i*2/6.5,i*5.0))
vs.append((i*2/2.2,i*2/9,i*5.0))
vs.append((0,i*2/9,i*5.0))
vs.append((i*2/2.2,-i*2/9,i*5.0))
vs.append((0,-i*2/9,i*5.0))
vs.append((i*2/2.2,i*2/9,i*3.7))
vs.append((0,i*2/9,i*3.7))
vs.append((i*2/2.2,-i*2/9,i*3.7))
vs.append((0,-i*2/9,i*3.7))
vs.append((i*2/1.9,i*2/6.5,i*3.7))
vs.append((0,i*2/6.5,i*3.7))
vs.append((i*2/1.9,-i*2/6.5,i*3.7))
vs.append((0,-i*2/6.5,i*3.7))
vs.append((i*2/1.8 ,i*2/6.5,i*3.2))
vs.append((0 ,i*2/6.5,i*3.2))
vs.append((i*2/1.8 ,-i*2/6.5,i*3.2))
vs.append((0 ,-i*2/6.5,i*3.2))
#legs
vs.append(( i*2/2.1, i*2/7.6, i*3.2))
vs.append(( 0.4,i*2/7.6, i*3.2))
vs.append(( i*2/2.1, -i*2/7.6, i*3.2))
vs.append(( 0.4,-i*2/7.6, i*3.2))
vs.append((i*2/2.1,i*2/7.6,i*2.35))
vs.append((0.4,i*2/7.6,i*2.35))
vs.append((i*2/2.1,-i*2/7.6,i*2.35))
vs.append((0.4,-i*2/7.6,i*2.35))
vs.append((i*2/1.95,i*2/6.6,i*2.35))
vs.append((0.35,i*2/6.6,i*2.35))
vs.append((i*2/1.95,-i*2/6.6,i*2.35))
vs.append((0.35,-i*2/6.6,i*2.35))
vs.append((i*2/1.95,i*2/6.6,i*1.8))
vs.append((0.35,i*2/6.6,i*1.8))
vs.append((i*2/1.95,-i*2/6.6,i*1.8))
vs.append((0.35,-i*2/6.6,i*1.8))
vs.append((i*2/2.1,i*2/7.6,i*1.8))
vs.append((0.4,i*2/7.6,i*1.8))
vs.append((i*2/2.1,-i*2/7.6,i*1.8))
vs.append((0.4,-i*2/7.6,i*1.8))
vs.append((i*2/2.1,i*2/7.6,i*.85))
vs.append((0.4,i*2/7.6,i*.85))
vs.append((i*2/2.1,-i*2/7.6,i*.85))
vs.append((0.4,-i*2/7.6,i*.85))
vs.append((i*2/1.8, i*2/4.6,i*.85))
vs.append((0.28, i*2/4.6,i*.85))
vs.append((i*2/1.8, -i*2/1.5,i*.85))
vs.append((0.28, -i*2/1.5,i*.85))
vs.append((i*2/1.8, i*2/4.6,i*.0))
vs.append((0.28, i*2/4.6,i*.0))
vs.append((i*2/1.8, -i*2/1.5,i*.0))
vs.append((0.28, -i*2/1.5,i*.0))
#hands
vs.append((i*2/3,i*2/5.5,i*6.59))
vs.append((i*2/3,-i*2/5.5,i*6.59))
vs.append((i*2/3,-i*2/5.5,i*5.8))
vs.append((i*2/3,i*2/5.5,i*5.8))
vs.append((i*2/1.46,i*2/5.5,i*6.59))
vs.append((i*2/1.46,-i*2/5.5,i*6.59))
vs.append((i*2/1.46,-i*2/5.5,i*5.8))
vs.append((i*2/1.46,i*2/5.5,i*5.8))
vs.append((i*2/1.46,i*2/7.5,i*6.49))
vs.append((i*2/1.46,-i*2/7.5,i*6.49))
vs.append((i*2/1.46,-i*2/7.5,i*5.9))
vs.append((i*2/1.46,i*2/7.5,i*5.9))
vs.append((i*2.1,i*2/7.5,i*6.49))
vs.append((i*2.1,-i*2/7.5,i*6.49))
vs.append((i*2.1,-i*2/7.5,i*5.9))
vs.append((i*2.1,i*2/7.5,i*5.9))
vs.append((i*2.1,i*2/5.5,i*6.59))
vs.append((i*2.1,-i*2/5.5,i*6.59))
vs.append((i*2.1,-i*2/5.5,i*5.8))
vs.append((i*2.1,i*2/5.5,i*5.8))
vs.append((i*2.81,i*2/5.5,i*6.59))
vs.append((i*2.81,-i*2/5.5,i*6.59))
vs.append((i*2.81,-i*2/5.5,i*5.8))
vs.append((i*2.81,i*2/5.5,i*5.8))
vs.append((i*2.81,i*2/7.5,i*6.39))
vs.append((i*2.81,-i*2/7.5,i*6.39))
vs.append((i*2.81,-i*2/7.5,i*6))
vs.append((i*2.81,i*2/7.5,i*6))
vs.append((i*3.7,i*2/7.5,i*6.39))
vs.append((i*3.7,-i*2/7.5,i*6.39))
vs.append((i*3.7,-i*2/7.5,i*6))
vs.append((i*3.7,i*2/7.5,i*6))
vs.append((i*3.7,i*2/6.5,i*6.49))
vs.append((i*3.7,-i*2/6.5,i*6.49))
vs.append((i*3.7,-i*2/6.5,i*5.9))
vs.append((i*3.7,i*2/6.5,i*5.9))
vs.append((i*4.2,i*2/6.5,i*6.49))
vs.append((i*4.2,-i*2/6.5,i*6.49))
vs.append((i*4.2,-i*2/6.5,i*5.9))
vs.append((i*4.2,i*2/6.5,i*5.9))
vs.append((i*4.2,i*2/8.5,i*6.39))
vs.append((i*4.2,-i*2/6.5,i*6.39))
vs.append((i*4.2,-i*2/6.5,i*6.1))
vs.append((i*4.2,i*2/8.5,i*6.1))
vs.append((i*4.5,i*2/8.5,i*6.39))
vs.append((i*4.5,-i*2/4.5,i*6.39))
vs.append((i*4.5,-i*2/4.5,i*6.1))
vs.append((i*4.5,i*2/8.5,i*6.1))
vs.append((i*4.6,i*2/8.5,i*6.39))
vs.append((i*4.6,-i*2/4.5,i*6.39))
vs.append((i*4.6,-i*2/4.5,i*6.1))
vs.append((i*4.6,i*2/8.5,i*6.1))
vs.append((i*4.6,i*2/8.5,i*6.39))
vs.append((i*4.6,-i*2/8.5,i*6.39))
vs.append((i*4.6,-i*2/8.5,i*6.1))
vs.append((i*4.6,i*2/8.5,i*6.1))
vs.append((i*4.8,i*2/8.5,i*6.39))
vs.append((i*4.8,-i*2/8.5,i*6.39))
vs.append((i*4.8,-i*2/8.5,i*6.1))
#139
vs.append((i*4.8,i*2/8.5,i*6.1))
#delete duplicates points (0,0,0) 0-139 = 0:(0,0,0)
ks = vs[:0] + vs[139:]
#
#for i2 in range(17):
##head
# if i2==1:
# tt=i2*8
# fs.append((tt-7,tt-6,tt-5))
# fs.append((tt-5,tt-4,tt-6))
# fs.append((tt-3,tt-2,tt-7))
# fs.append((tt-7,tt-2,tt-6))
# fs.append((tt-3,tt-1,tt-7))
# fs.append((tt-5,tt-1,tt-7))
# fs.append((tt-5,tt-1,tt-0))
# fs.append((tt-5,tt-4,tt-0))
##2throat
##3-4top5medium6low
##body#shoulder
# if i2>1 and i2<=3:
# tt=i2*8
# fs.append((tt-3,tt-2,tt-7))
# fs.append((tt-7,tt-2,tt-6))
# fs.append((tt-5,tt-1,tt-0))
# fs.append((tt-5,tt-4,tt-0))
##body
# if i2>3 and i2<7:
# tt=i2*8
# fs.append((tt-3,tt-2,tt-7))
# fs.append((tt-7,tt-2,tt-6))
# fs.append((tt-3,tt-1,tt-7))
# fs.append((tt-5,tt-1,tt-7))
# fs.append((tt-5,tt-1,tt-0))
# fs.append((tt-5,tt-4,tt-0))
##legs
##7-10legs
# if i2>=7 and i2<10:
# tt=i2*8
# fs.append((tt-3,tt-2,tt-7))
# fs.append((tt-7,tt-2,tt-6))
# fs.append((tt-3,tt-1,tt-7))
# fs.append((tt-5,tt-1,tt-7))
# fs.append((tt-5,tt-1,tt-0))
# fs.append((tt-5,tt-4,tt-0))
# fs.append((tt-6,tt-4,tt-0))
# fs.append((tt-6,tt-0,tt-2))
# if i2==10:
# tt=i2*8
# fs.append((tt-3,tt-2,tt-7))
# fs.append((tt-7,tt-2,tt-6))
# fs.append((tt-3,tt-1,tt-7))
# fs.append((tt-5,tt-1,tt-7))
# fs.append((tt-5,tt-1,tt-0))
# fs.append((tt-5,tt-4,tt-0))
# fs.append((tt-6,tt-4,tt-0))
# fs.append((tt-6,tt-0,tt-2))
# fs.append((tt-3,tt-2,tt-0))
# fs.append((tt-3,tt-1,tt-0))
##shoulder
# if i2==11:
# tt=i2*8
# fs.append((tt-3,tt-2,tt-7))
# fs.append((tt-7,tt-2,tt-6))
# fs.append((tt-6,tt-2,tt-5))
# fs.append((tt-1,tt-2,tt-5))
# fs.append((tt-7,tt-4,tt-0))
# fs.append((tt-7,tt-3,tt-0))
##hands
##11shoulder13cubit15bangle
# if i2>11:
# tt=i2*8
# fs.append((tt-3,tt-2,tt-7))
# fs.append((tt-7,tt-2,tt-6))
# fs.append((tt-6,tt-2,tt-5))
# fs.append((tt-1,tt-2,tt-5))
# fs.append((tt-5,tt-1,tt-0))
# fs.append((tt-5,tt-4,tt-0))
# fs.append((tt-7,tt-4,tt-0))
# fs.append((tt-7,tt-3,tt-0))
mesh = bpy.data.meshes.new('mesh')
mesh.from_pydata(ks,es,fs)
mesh.update(calc_edges=True)
mesh.validate(verbose=True)
#meshobj
meshobj= bpy.data.objects.new('mesh',mesh)
#collection
new_collection = bpy.data.collections.new('new_collection')
bpy.context.scene.collection.children.link(new_collection)
new_collection.objects.link(meshobj)
Вот генерация такого меша, который состоит из точек

Когда скрипт отработает надо выбрать точки счелкнуть ПКМ и выбрать Convert To -> Mesh (при генерации только точек c "mesh.update()" ).
Ниже показаны некоторые артефакты данного подхода - пофиксил удалением этих точек, соответственно если кому-то будет интересно делать на этой базе процедурную генерацию по параметрам имейте ввиду.

Вот как это выглядит
Ниже представлен уже собранный в полигоны, и прокинутый через миксамо меш, с тестовой анимацией Run

визуализация стыков

скелет в миксамо выбирался по принципу варешка и большой палец.
небольшое дополнение

в скрипте написан простенький процесс полигонизации, но дело в том что грудина должна быть как на рисунке с визуализацией стыков, так что делать точки может быть удобным (отсюда следует, что при начале подхода создания персонажа точки в пространстве по пропорциям ценнее чем полностью собранный меш, это не исключает того факта что можно создавать по чувству как художник или делать риг по концептам )
kesenkai
А в чем, собственно, цель этого извращения? Зачем в ручную прописывать координаты каждой вершины, тогда уже быстрее просто в редакторе кубов накидать) Другое дело, если бы они процедурно генерились через geo nodes, тогда смысл был бы
Jijiki Автор
показал как при помощи математики и решении двух интегралов записывая это в список вершин можно собрать на языке программирования например такого болванчика, то что записано вручную можно представить в виде формулы (в данном случае 1 интеграл сверху вниз, второй слева вправо), второй момент - делал сначала кубами, там несколько неудобно получается, телодвижений больше, кинув 1 формулу, еффект всегда 1, если делать на глаз еффект может быть разный, можно прям кубами сделать, в данной статье предпологается что модель будет полая и будет соответствовать анимированию скелетки с миксамо, которая требует чтоб все вершины были соединены(кроме тех вершин которые находятся на центральной оси, результат скрипта требуется отзеркалить )
Скрытый текст
это тоже является процедурной генерацией, но просто накладно писать соединения треугольников учитывая 2-3 вылетевшие точки к основанию, хотя они императивно вылетают их просто можно удалить
Jijiki Автор
скорее всего 3 интеграла