Давайте пограємо зі змією/Скриптуємо Блендер
Нащо воно взагалі треба?
ред.Якщо ми хочемо просунутись до програмування трьохвимірних ігор, нам обов'язково треба включити в свій арсенал хоч якийсь редактор. Бо навіть простенька модель кубика, якщо її малювати вручну, з коду буде виглядати так:
glBegin(GL_POLYGON);
glVertex3f(1.000000,1.000000,-1.000000);
glVertex3f(1.000000,-1.000000,-1.000000);
glVertex3f(-1.000000,-1.000000,-1.000000);
glVertex3f(-1.000000,1.000000,-1.000000);
glEnd();
glBegin(GL_POLYGON);
glVertex3f(1.000000,0.999999,1.000000);
glVertex3f(-1.000000,1.000000,1.000000);
glVertex3f(-1.000000,-1.000000,1.000000);
glVertex3f(0.999999,-1.000001,1.000000);
glEnd();
glBegin(GL_POLYGON);
glVertex3f(1.000000,1.000000,-1.000000);
glVertex3f(1.000000,0.999999,1.000000);
glVertex3f(0.999999,-1.000001,1.000000);
glVertex3f(1.000000,-1.000000,-1.000000);
glEnd();
glBegin(GL_POLYGON);
glVertex3f(1.000000,-1.000000,-1.000000);
glVertex3f(0.999999,-1.000001,1.000000);
glVertex3f(-1.000000,-1.000000,1.000000);
glVertex3f(-1.000000,-1.000000,-1.000000);
glEnd();
glBegin(GL_POLYGON);
glVertex3f(-1.000000,-1.000000,-1.000000);
glVertex3f(-1.000000,-1.000000,1.000000);
glVertex3f(-1.000000,1.000000,1.000000);
glVertex3f(-1.000000,1.000000,-1.000000);
glEnd();
glBegin(GL_POLYGON);
glVertex3f(1.000000,0.999999,1.000000);
glVertex3f(1.000000,1.000000,-1.000000);
glVertex3f(-1.000000,1.000000,-1.000000);
glVertex3f(-1.000000,1.000000,1.000000);
glEnd();
А в ньому всього то шість граней. І то я полінувався вводити все вручну, і згенерував цей кубик скриптом з блендера. Тепер порахуйте скільки граней у цієї лисої пані:
А якби вона була не лиса? От тому без 3d редактора ми нікуди не просунемось. Обрати Blender варто вже тому, що він безкоштовний, функціональний, має вбудований ігровий рушій, і скриптується нашим улюбленцем Пайтоном.
Щоб трохи розібратись з інтерфейсом Блендера який спочатку шокує, майже як штурвал літака, варто сісти на кілька годин за підручник, і потицяти кнопки. Про Блендер можна прочитати:
- Blender 3D: Noob to Pro - англійський вікіпідручник, один з кращих які можна знайти в мережі.
- Blender: первые шаги в Python
- Blender - вікіпідручник, в зародковій стадії, його треба не читати, а скоріше писати.
Привіт, світе!
ред.Скрипти можна писати як у вбудованому редакторі Блендера, так і в зовнішньому. Вбудований може бути незручним для вас, якщо ви звикли до певних комбінацій клавіш. Наприклад, щоб копіювати та вставити текст з буфера обміну, доведеться користуватись комбінаціями CTR+SHIFT-C
та CTR+SHIFT+V
відповідно.
Скрипти зберігаються в каталозі ~/.blender/scripts
у лінуксі, та там де ви встановили блендер\.blender\scripts
у windows. Тепер створимо там маленький скрипт. Якось його називаємо. Наприклад hello.py
.
Щоб мати поняття що ми хочемо зробити можна подивитись коротке трихвилинне відео.
#!BPY
"""
Name: 'Hello'
Blender: 244
Group: 'Export'
Tooltip: 'Our test script'
"""
import Blender
import bpy
def write(filename):
out = file(filename, "w")
sce= bpy.data.scenes.active
for ob in sce.objects:
out.write(ob.type + ": " + ob.name + "\n")
out.close()
Blender.Window.FileSelector(write, "Export")
Тепер повертаємось до вікна скриптів, і в його меню Scripts вибираємо пункт Update Menus. Після чого шукаємо hello в Scripts -> Export. Натискаємо. У вікні скриптів з'являється вікно вибору файлу. Вводимо якусь назву, і натиснувши Export отримаємо там текстовий файл з приблизно таким вмістом:
Mesh: Cube Lamp: Lamp Camera: Camera
Якщо звісно ми нічого не додавали на нашу сцену.
Як воно працює?
Розглянемо наш скрипт рядок за рядком:
#!BPY
Перший рядок каже блендеру, що це скрипт блендера, і що його треба додати в меню скануючи папки зі скриптами. Далі йде багаторядкова документація. Вона складається з чотирьох рядків
Name: 'Hello' Blender: 244 Group: 'Export' Tooltip: 'Our test script'
які допомагають блендеру правильно розмістити скрипт в меню. Name - так буде називатись пункт меню. Group - він буде поміщений в таке меню. Tooltip - підказка що буде з'являтись при затримці курсора над пунктом меню. Ну, а Blender - номер версії для якої написаний скрипт.
Далі завантажуються два модулі:
import Blender import bpy
Перший модуль є основним для написання скриптів, другий загалом повторює його функціональність, та поки що є експериментальним. Він мав би надавати зручніший доступ до даних та функцій блендера, та поки що його вміст часто змінюють. Модуль Blender поки що задокументований краще.
Далі ми створюємо функцію, яка пише щось у файл:
def write(filename): out = file(filename, "w")
Це просто. Далі цікавіше:
sce= bpy.data.scenes.active for ob in sce.objects: out.write(ob.type + ": " + ob.name + "\n")
Наступні три рядки беруть поточну сцену, і для всіх об'єктів сцени виводять у файл тип об'єкта, та через дві крапки його ім'я. Потім файл закривається і функція закінчується.
А остання функція:
Blender.Window.FileSelector(write, "Export")
Відображає вікно вибору файла, з кнопкою "Export". Після того як її натиснуть викликає функцію write
передавши їй ім'я файлу.
Взагалі то, цей скрипт майже нічого не робить, зате демонструє нам загальний принцип роботи розширень Блендера.
Експорт мешу
ред.Щоб розібратись з форматом трикутного мешу у Блендері, найкраще було б написати скрипт, яка виписує дані мешу у зовнішній файл. Наприклад можна написати програму, що генерує код OpenGL який малює наш об'єкт. Для цього треба замінити три основні рядки в попередньому скрипті.
Спочатку знайти вибраний об'єкт:
sce = bpy.data.scenes.active
ob = sce.objects.active
Потім отримати його меш:
mesh = ob.getData(mesh=1)
Хоча ми могли і отримати не меш. Якщо наприклад вибраним об'єктом було джерело світла. А воно не має ніякої трикутної сітки. Тому, в коді що піде далі варто ловити виключення. Якось так:
try:
for face in mesh.faces:
out.write("glBegin(GL_POLYGON);\n")
for vert in face.v:
c=mesh.verts[vert.index].co
out.write("glVertex3f(%f,%f,%f);\n" % (c.x,c.y,c.z) )
out.write("glEnd();\n\n")
except:
print "Selected something without mesh. Nothing exported."
Тобто від пробує виконати код в секції try, а якщо не виходить, то жаліється в консоль. Варто зауважити, що при тестуванні скриптів до блендера, бажано щоб він був запущений з консолі, тоді в ній будуть з'являтись повідомлення про помилки. Він вам прямо так і скаже: , але не скаже де ту консоль шукати, тому вам варто знати самим.
Тепер по коду. mesh.faces
як вже можна було здогадатись - список граней нашої моделі. Кожна грань має список своїх вершин - атрибут v
. Кожна вершина списку має атрибут .index
, в якому зберігається індекс цієї вершини - її номер в загальному списку вершин. (Очевидно що одна вершина зазвичай входить в більш ніж одну грань, тому зберігати її координати по кілька раз - неефективно.
Ну, а mesh.verts
- список вершин. Основні атрибути в вершини це co
- координати, та no
- нормаль. Що цікаво, нормалі також є і у кожної грані. Які з них впливають на затінення - визначається кнопками set smooth
та set solid
на панелі кнопок у вкладці Editing -> Link and materials
. Але туди лізти поки що не варто.
Генерація свого мешу
ред.Трикутник
ред.Тепер давайте намалюємо свій меш. Що небудь попростіше. Найпростіше - трикутник, з нього і почнемо. Ось функція яка його створює:
def createtriangle():
obj=NMesh.GetRaw()
v1=NMesh.Vert(0,10,0)
v2=NMesh.Vert(-1,0,0)
v3=NMesh.Vert(1,0,0)
obj.verts.append(v1)
obj.verts.append(v2)
obj.verts.append(v3)
f=NMesh.Face()
f.v.append(obj.verts[0])
f.v.append(obj.verts[1])
f.v.append(obj.verts[2])
obj.faces.append(f)
NMesh.PutRaw(obj,"Triangle",1)
А тепер по порядку. Спочатку ми створюємо базовий об'єкт, з чистим мешем:
obj=NMesh.GetRaw()
Якщо цій функції передати параметром ім'я якогось існуючого об'єкта, то меш того об'єкта буде взятий за основу. Тобто побудова почнеться не з нуля. Тільки варто переконатись що такий об'єкт існує. Далі створимо якісь три вершини. Будь-які, основне - аби їх було не менше трьох, бо інакше ми не отримаємо жодної грані.
v1=NMesh.Vert(0,10,0)
v2=NMesh.Vert(-1,0,0)
v3=NMesh.Vert(1,0,0)
Потім додамо ці вершини у об'єкт.
obj.verts.append(v1)
obj.verts.append(v2)
obj.verts.append(v3)
Коли вершини створено, можна створювати грані. Починають з порожньої грані.
f=NMesh.Face()
А потім в неї додають потрібні вершини. З об'єкта. (Щоб він міг розібратись з індексами). Десь ось так:
f.v.append(obj.verts[0])
f.v.append(obj.verts[1])
f.v.append(obj.verts[2])
Після того як грань буде готова додаємо її в об'єкт.
obj.faces.append(f)
А так як наш об'єкт - трикутник, то цього достатньо. Залишилось тільки зробити так, щоб він появився у вікні блендера.
NMesh.PutRaw(obj,"Triangle",1)
Першим параметром передається об'єкт, що буде вставлятись в сцену, другим - його ім'я. Якщо ім'я унікальне - створиться новий об'єкт. Якщо ні - то той об'єкт що носив це ім'я раніше заміниться на наш новий. Тому, будьте обережні. Третій параметр - прапор, що вказує блендеру, про необхідність обчислення нормалей. Нуль - ми задали нормалі. Інакше - обчислити їх.
Карта висот
ред.Тепер давайте зробимо щось поскладніше. Наприклад згенеруємо карту висот за функцією. Вони бувають доволі симпатичні:
Зразу покажу скрипт який це намалював:
#!BPY
"""
Name: 'Function heightmap'
Blender: 244
Group: 'Mesh'
Tooltip: 'Draw function heightmap mesh'
"""
import Blender
from Blender import NMesh
import bpy
import math
def f(x,y):
return (x*x+y*y)/5
def genheightmap(f,minx=-5,miny=-5,maxx=5,maxy=5,resolution=0.1,name="heightmap"):
width=int( (maxx-minx)/resolution)+1
height = int( (maxy - miny) / resolution)+1
maxx=minx+resolution*width
maxy=miny+resolution*height
obj=NMesh.GetRaw()
y=miny
obj=NMesh.GetRaw()
for y in range(height):
for x in range(width):
fx=minx+x*resolution
fy=miny+y*resolution
z=f(fx,fy)
v=NMesh.Vert(fx,fy,z)
obj.verts.append(v)
for y in range(height-1):
for x in range(width-1):
f=NMesh.Face()
f.v.append(obj.verts[y*width + x])
f.v.append(obj.verts[(y+1)*width + x])
f.v.append(obj.verts[y*width + x + 1])
obj.faces.append(f)
f=NMesh.Face()
f.v.append(obj.verts[y*width + x + 1])
f.v.append(obj.verts[(y+1)*width + x + 1])
f.v.append(obj.verts[(y+1)*width + x])
obj.faces.append(f)
NMesh.PutRaw(obj,name,1)
genheightmap(f,-5,-5,5,5,0.7)
Він майже аналогічний скрипту що малював трикутник, з тією відмінністю, що трикутників тут набагато більше. Принцип роботи дуже простий. Подивимось на нашу карту висот в режимі дротяної моделі. Функцію я регулярно змінював, бо цікаво ж.
Може здатись що топологія дуже складна, та насправді, якщо глянути зверху, ми побачим звичайну однорідну сітку:
Тому, нам спочатку треба обчислити координати вершин на однорідному розбитті, та заповнити ними модель:
for y in range(height):
for x in range(width):
fx=minx+resolution*x
fy=miny+resolution*y
z=f(fx,fy)
v=NMesh.Vert(fx,fy,z)
obj.verts.append(v)
А потім, об'єднати їх в трикутники, по два трикутники на клітинку, як на попередній ілюстрації. Зауважте, що якщо ми маємо width
вершин в ряді, то клітинок в ряді буде width-1
.
Наш скрипт чудово працює, але є одне але. Щоб змінити функцію нам треба міняти його код. А це незручно. Зручніше було б, якщо б скрипт давав нам можливість ввести свою функцію, точність, межі на яких її обчислювати. А для цього нам потрібний якийсь інтерфейс.
Створення графічного інтерфейсу
ред.Графічний інтерфейс у блендері створюється ну просто дуже просто. Простіше хіба що в Delphi. За створення інтерфейсу відповідає модуль Blender.Draw
. І одна команда:
Blender.Draw.Register(draw,event,button)
Ця команда приймає в параметрах три функції, які вона запускає в разі необхідності:
draw
- функція що перемальовує інтерфейс.event
- функція що обробляє події, такі як рухи миші, та натиснення клавіш клавіатури.button
- функція що обробляє події натискання кнопок графічного інтерфейсу.
Давайте поглянемо одразу на можливий інтерфейс для нашого скрипта:
Його малює така функція:
def draw():
Blender.BGL.glClear(Blender.BGL.GL_COLOR_BUFFER_BIT)
Blender.Draw.Toggle("Create given surface",1,10,10,150,20,0)
Blender.Draw.Toggle("Create sample surface",2,10,30,150,20,0)
Blender.Draw.String("f(x,y)=",3,160,50,180,20,functiontext,200,"Input surface function here",changestring)
Blender.Draw.String("Surface name: ",4,10,50,150,20,surfacename,100,"What name will be given to created object.",changestring)
Blender.Draw.String("min_x: ",5,160,30,125,20,str(minx),10,"Minimal x value",changestring)
Blender.Draw.String("min_y: ",6,285,30,125,20,str(miny),10,"Minimal y value",changestring)
Blender.Draw.String("max_x: ",7,160,10,125,20,str(maxx),10,"Maximal x value",changestring)
Blender.Draw.String("max_y: ",8,285,10,125,20,str(maxy),10,"Maximal y value",changestring)
Blender.Draw.String("d: ",9,340,50,70,20,str(resolution),10,"Size of one grid cell",changestring)
Перший рядок функції
Blender.BGL.glClear(Blender.BGL.GL_COLOR_BUFFER_BIT)
очищає все поле для малювання. Якщо ви програмували в OpenGL - ця функція може видатись вам знайомою. А все тому, що інтерфейс блендера виводиться з допомогою OpenGL. І таке рішення видається досить вдалим.
Наступна функція
Blender.Draw.Toggle("Create given surface",1,10,10,150,20,0)
Створює кнопку - переключатель. Перший параметр - напис на кнопці. Другий - її унікальний номер. За цим номером обробник події дізнається від якої кнопки прийшов сигнал. Тому бажано щоб кожен елемент інтерфейсу мав унікальний номер, якщо він звісно не дублює функцію якогось іншого. Два інші числа - координати кнопки в пікселях. Що цікаво, тут на відміну від інших фреймворків інтерфейсу координати математичні. Тобто нуль внизу, та ігрик направлений вверх.
Далі ширина та висота кнопки, Останнє число - початковий стан кнопки. 1 - натиснута, 0 - відпущена. В кінці ще можна додавати рядок з спливаючою підказкою, та якщо підпис достатньо інформативний в цьому немає необхідності.
Наступний оператор аналогічно створює подібну кнопку. Але ми малювали ще елемент іншого виду - для вводу тексту:
Blender.Draw.String("f(x,y)=",3,160,50,180,20,functiontext,200,"Input surface function here",changestring)
Перший параметр - підпис. Цей підпис буде стояти зліва від тексту що ми будемо вводити, і буде займати ширину нашого компонента. Тому упевніться що і для вводу виділено достатньо. Наступні п'ять параметрів як і у кнопки - переключателя: ідентифікатор події, координати, розмір. Після них йде рядок (functiontext
) - у якому вказують текст що буде стояти в полі для вводу за замовчуванням. Після нього - число (200) - максимальна кількість символів для вводу (не більше 399). Потім підказка, і останнє - функція зворотнього виклику changestring
. Ця функція викликається після введення тексту, і їй передаються два аргументи - спершу ідентифікатор події, а потім параметр.
Стандартний обробник button
, що передавався Blender.Draw.Register
нам не підійшов, бо в його параметрах є тільки event
- ідентифікатор компонента що запустив подію. А нам ще треба отримувати введений текст.
Розглянемо спочатку обробник button
:
def button(evt):
if evt == 1: # Якщо натиснута кнопка з ідентифікатором 1
genheightmap(f,minx,miny,maxx,maxy,resolution,functiontext) # Створити карту висот, з заданими параметрами
Blender.Window.Redraw() # Та обновити вміст вікон блендера
Ось так просто. Так само туди вписуємо обробку подій для інших кнопок. if evt == 2
і так далі.
Функція - обробник спеціально для текстів нічим не складніша. Текст передається у змінну val
. Єдиний недолік - вивід проводиться через глобальні змінні, та я не знаю як можна зробити інакше.
def changestring(event,val):
if event==3:
global functiontext
functiontext=val
Якщо кнопка для вводу тексту мала ідентифікатор 3, і функцію обробник - changestring
, то з її допомогою можна було б змінювати вміст глобальної змінної functiontext
.
Нам залишилось розглянути тільки функцію:
def event(evt,val):
if evt == Blender.Draw.ESCKEY:
Blender.Draw.Exit()
return
Вона майже нічого не робить, окрім того, що при натисненні клавіші Esc припиняє роботу скрипта.
Список всіх констант для клавіш, та інших подій можна знайти на сторінках документації.
Ну, і звісно спочатку варто було зареєструвати глобальні змінні, та створити функцію що малює поверхню:
functiontext="0"
surfacename="heightmap1"
minx=-5
miny=-5
maxx=5
maxy=5
resolution=0.2
def f(x,y):
return eval(functiontext,{"x":x,"y":y,"sin":math.sin,"atan2":math.atan2})
Увесь скрипт цілком виглядає так:
#!BPY
"""
Name: 'Function heightmap'
Blender: 244
Group: 'Mesh'
Tooltip: 'Draw function heightmap mesh'
"""
import Blender
from Blender import NMesh
import bpy
import math
functiontext="0"
surfacename="heightmap1"
minx=-5
miny=-5
maxx=5
maxy=5
resolution=0.2
def f(x,y):
return eval(functiontext,{"x":x,"y":y,"sin":math.sin,"atan2":math.atan2})
def f2(x,y):
r=math.sqrt(x*x+y*y)
return 5*math.sin(r)/(r+1)
def genheightmap(f,minx=-5,miny=-5,maxx=5,maxy=5,resolution=0.1,name="heightmap"):
width=int( (maxx-minx)/resolution)+1
height = int( (maxy - miny) / resolution)+1
maxx=minx+resolution*width
maxy=miny+resolution*height
obj=NMesh.GetRaw()
y=miny
obj=NMesh.GetRaw()
for y in range(height):
for x in range(width):
fx=minx+x*resolution
fy=miny+y*resolution
z=f(fx,fy)
v=NMesh.Vert(fx,fy,z)
obj.verts.append(v)
for y in range(height-1):
for x in range(width-1):
f=NMesh.Face()
f.v.append(obj.verts[y*width + x])
f.v.append(obj.verts[(y+1)*width + x])
f.v.append(obj.verts[y*width + x + 1])
obj.faces.append(f)
f=NMesh.Face()
f.v.append(obj.verts[y*width + x + 1])
f.v.append(obj.verts[(y+1)*width + x + 1])
f.v.append(obj.verts[(y+1)*width + x])
obj.faces.append(f)
NMesh.PutRaw(obj,name,1)
def changestring(event,val):
if event==3:
global functiontext
functiontext=val
if event==4:
global surfacename
surfacename=val
if event==5:
global minx
minx=float(val)
if event==6:
global miny
miny=float(val)
if event==7:
global maxx
maxx=float(val)
if event==8:
global maxy
maxy=float(val)
if event==9:
global resolution
resolution=float(val)
def draw():
Blender.BGL.glClear(Blender.BGL.GL_COLOR_BUFFER_BIT)
Blender.Draw.Toggle("Create given surface",1,10,10,150,20,0,)
Blender.Draw.Toggle("Create sample surface",2,10,30,150,20,0,)
Blender.Draw.String("f(x,y)=",3,160,50,180,20,functiontext,200,"Input surface function here",changestring)
Blender.Draw.String("Surface name: ",4,10,50,150,20,surfacename,100,"What name will be given to created object.",changestring)
Blender.Draw.String("min_x: ",5,160,30,125,20,str(minx),10,"Minimal x value",changestring)
Blender.Draw.String("min_y: ",6,285,30,125,20,str(miny),10,"Minimal y value",changestring)
Blender.Draw.String("max_x: ",7,160,10,125,20,str(maxx),10,"Maximal x value",changestring)
Blender.Draw.String("max_y: ",8,285,10,125,20,str(maxy),10,"Maximal y value",changestring)
Blender.Draw.String("d: ",9,340,50,70,20,str(resolution),10,"Size of one grid cell",changestring)
def event(evt,val):
if evt == Blender.Draw.ESCKEY:
Blender.Draw.Exit()
return
def button(evt):
if evt == 1:
genheightmap(f,minx,miny,maxx,maxy,resolution,functiontext)
Blender.Window.Redraw()
if evt == 2:
genheightmap(f2,-5,-5,5,5,0.1,"surface2")
Blender.Window.Redraw()
Blender.Draw.Register(draw,event,button)