Всем привет. Сегодня будет заключительная статья на тему программирования в мире 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

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

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

в скрипте написан простенький процесс полигонизации, но дело в том что грудина должна быть как на рисунке с визуализацией стыков, так что делать точки может быть удобным (отсюда следует, что при начале подхода создания персонажа точки в пространстве по пропорциям ценнее чем полностью собранный меш, это не исключает того факта что можно создавать по чувству как художник или делать риг по концептам )