Полигональные сетки
Что это за сетки?
И снова здравствуйте, на очередном уроке, который я решил посвятить теме создания полигональных сеток в Shockwave3D.
Для начала уясним, о каких сетях идет речь. Все объекты состоят из набора многоугольников, которые формируют видимую оболочку. Каждый многоугольник состоит из набора, последовательно соединенных вершин (точек). Поскольку одна вершина может быть использована в нескольких многоугольниках, то их, как правило, записывают в отдельный список и используют индексы (порядковые номера в списке). Минимальным многоугольником, всегда лежащим в одной плоскости является треугольник, поэтому именно он используется при формировании сеток. Визуально объект, построенный таким образом напоминает сетку из треугольников. Вот небольшое изображение, иллюстрирующее такой подход:
Дальше к координатам сетки можно привязать координаты текстуры (т.н. UV-текстуры), или задать цвета отдельных вершин, создавая градацию цвета.
Вообще-то потребность в создании сеток на низком уровне, в практической жизни, возникает довольно редко, так как все необходимое можно реализовать непосредственно в Вашем 3D редакторе и экспортировать w3d формат (об этом будет отдельный урок). Единственный случай, когда такое может понадобиться, это когда сетка должна динамически изменяться в процессе работы приложения. Так моделируют, например, поведение воды или изменения ландшафта. Лично я впервые использовал динамическую генерацию сетки только при создании гибкого прута в двигателе Грина (прут генерируется вокруг кривой Безье).
Проектирование
Теперь давайте перейдем непосредственно к изучению того, как реализовать построение сеток. Как и раньше открываем etalon.dir в Macromedia Director и пытаемся создать нечто простенькое. А простеньким у нас сегодня будет пирамида с квадратным основанием (пример взят из хелпы по Director-у и основательно переработан).
on beginSprite me
initScene(sprite(me.spriteNum), 200, -30, -25)
-- создаем ресурс - пустую сетку
nm = scene.newMesh("pyramid", 6 , 5, 0, 3, 4)
-- наполняем список вершин координатами
nm.vertexList = [ vector(0, 0, 0), vector(40, 0, 0), vector(40, 0, 40), vector(0, 0, 40), vector(20, 50, 20) ]
-- наполняем список цветов
nm.colorList = [ rgb(255, 0, 0), rgb(0, 255, 0), rgb(0, 0, 255) ]
-- наполняем список координат текстур
nm.texcoordList = [ [0, 0], [2, 0], [2, 2], [0, 2] ]
-- наполняем список треугольников, присвоив цепочки индексов
nm.face[1].vertices = [ 4, 1, 2 ]
nm.face[2].vertices = [ 4, 2, 3 ]
nm.face[3].vertices = [ 5, 2, 1 ]
nm.face[4].vertices = [ 5, 3, 2 ]
nm.face[5].vertices = [ 5, 4, 3 ]
nm.face[6].vertices = [ 5, 1, 4 ]
-- наполняем список индексов цветов для градиента
nm.face[1].colors = [3, 2, 3]
nm.face[2].colors = [3, 3, 2]
nm.face[3].colors = [1, 3, 2]
nm.face[4].colors = [1, 2, 3]
nm.face[5].colors = [1, 3, 2]
nm.face[6].colors = [1, 2, 3]
-- привязка шедера к дну пирамиды
nm.face[1].shader = scene.shader[1]
nm.face[2].shader = scene.shader[1]
-- привязка текстурных координат к вершинам полигона
nm.face[1].textureCoordinates = [4, 1, 2]
nm.face[2].textureCoordinates = [4, 2, 3]
-- генерируем нормали к поверхности
nm.generateNormals(#flat)
-- генерируем сетку
nm.build()
-- создаем видимый объект на основе ресурса сетки
nm = scene.newModel("Pyramid1", nm)
end
Выглядит довольно симпатично. Теперь подробнее.
Функция newMesh(name, numFaces, numVertices, numNormals, numColors, numTextureCoordinates) создает сетку с параметрами:
- name - название ресурса сетки;
- numFaces - количество полигонов (треугольников) в сетке;
- numVertices - количество вершин (точек) в списке вершин;
- numNormals - количество нормалей к поверхностям. Обычно ставят 0, поскольку нормали можно генерировать автоматически (функция generateNormals). Нормаль, это вектор перпендикуляра к поверхности, задающий направление отражения света. Меняя направление векторов нормали в вершинах можно имитировать изгиб поверхности (до определенного уровня);
- numColors - количество цветов в списке. Позже номер цвера используется для создания плавного градиента плоскости;
- numTextureCoordinates - количество координат текстуры в списке;
Если снова взглянуть на пример, то можно сказать, что newMesh(6 , 5, 0, 3, 4), означает создание сетки в которой мы резервируем место под пять вершин с шестью полигонами, тремя цветами для градиента и четырьмя текстурными координатами.
Далее идет наполнение списков vertexList (список вершин), colorList (список цветов) и texcoordList (список двоек с координатами текстуры) содержимым (об этом далее по тексту). Далее следуют строки, служащие для заполнения списка треугольных полигонов face:
- face[index].colors - для полигона index устанавливает тройку индексов цветов для каждой вершины;
- face[index].normals - для полигона index устанавливает тройку индексов нормалей для каждой вершины;
- face[index].shader - для полигона index задается материал;
- face[index].textureCoordinates - для полигона index задается тройка индексов в списке координат текстуры;
- face[index].vertices - для полигона index задается тройка индексов в списке координат вершин;
Далее мы запускаем процедуру автоматической генерации нормалей к поверхности generateNormals. В результате использования мы получим заполненный список normalList и face[index].normals для всех полигонов. Процедуру заполнения можно проводить и вручную, но это очень хлопотно и оправданно лишь для особых случаев. Параметр #flat процедуры указывает на то, что поверхность должна иметь плоские грани. Если же вместо #flat подставить #smooth, то поверхность будет казаться гладкой.
Процедура build завершает создание ресурса сетки. После ее использования можно, при помощи newModel, создавать видимые клоны (то-есть объекты). Что можно делать с объектами мы уже обсуждали в предыдущих уроках.
А сейчас наступил черед оживить сетку. Собственно то, ради чего и стоит терпеть весь головняк, описанный выше. Давайте заставим вершину пирамиды двигаться по кругу. Для этого нужно добавить в начало скрипта объявление свойств:
property a, nm
тоесть текущего угла а и структуры сетки nm. Также в конец процедуры beginSprite нужно добавить инициализацию угла а начальным значением:
...
a = 0.0
end
И изменим процедуру calcMovie так, чтобы заставить пятую вершину вращаться вокруг оси oY:
on calcMovie() me
a = a + 5.0
a_rad = a * PI / 180.0
nm.vertexList[5] = vector(cos(a_rad) * 30.0, 50, sin(a_rad) * 30.0)
nm.generatenormals(#flat)
nm.build()
end
Выглядеть это будет приблизительно так (но только в движении):
а качнуть исходник можно отсюда. Ну вот, собственно, я и проиллюстрировал все приемы и методы, используемые при создании моделей на основе полигональных сеток в Shockwave3D. Теперь нужно сказать несколько слов про особенности расчетов собственной сетки.
Особенности заполнения сетки
Первый нюанс, о котором нужно упомянуть в этой главе заключается в последовательности заполнения треугольников. У треугольного полигона есть две стороны: лицевая и обратная. В нормальном режиме наблюдателю видна только лицевая сторона объекта. Лицевой сторона будет лишь в том случае, если порядок обхода вершин, при взгляде наблюдателя, производится по часовой стрелке. Следующий рисунок демонстрирует этот принцип:
Важным моментом, также является то, что вершины не должны находиться на одной прямой, поскольку тогда невозможно будет генерировать вектор нормали.
Второй момент, касается наложения текстуры на полигон. А именно того, как вычислить координаты текстуры.
Суть процеса хорошо показывает следующая иллюстрация:
Как можно видеть, на поверхности текстуры, можно определить любую точку, указав ей координату от 0.0 до 1.0 по вертикали и по горизонтали. Далее индексы набора таких точек присваиваются соответствующим вершинам полигона и текстура отображается на поверхности, в соответствии с задумкой автора.
В моем примере координаты текстуры выходят за пределы 0.0-1.0, потому, что текстура является циклически повторяемой, что позволило, имея маленький фрагмент, многократно его размножить.
Пример покруче
Динамическое моделирование сеток выгодно использовать для создания моделей по заранее известному алгоритму. Например, если форма объекта определяется некоторой формулой. Чтобы оценить всю красоту и мощь полигональных сеток в Shockwave3D, давайте создадим сплайн (просто кубическую кривую) и заставим его извиваться случайным образом.
Приведу здесь полный текст скрипта, поскольку писанины там хватает:
global sprite_w3d, scene
global old_move_point, move_point, click_point, move_delta
global model_focused, model_selected
property p0, p1, p2, p3, r -- формирующие точки и радиус
property nSeg, nDet -- уровень детализации кривой
property lVerts, obj
property res
property v2, v3
property l_val, delta_v
on beginSprite me
initScene(sprite(me.spriteNum), 1600, 0, 0)
p0 = vector(0.0, 0.0, 0.0) -- к-та начала кривой
p1 = vector(0.0, 100.0, 0.0) -- первая направляющая точка
p2 = vector(0.0, 200.0, 0.0) -- вторая направляющая точка
p3 = vector(0.0, 300.0, 0.0) -- к-та окончания кривой
r = 7.0
nSeg = 30 -- к-во сегментов вдоль кривой
nDet = 6 -- детализация поперечного сечения
nFaces = nSeg*(nDet)*2 -- к-во граней
nVerts = (nSeg + 1) * (nDet) -- к-во вершин
res = scene.newmesh("prut",
nFaces, nVerts, 0, 2)
res.colorlist = [rgb(0, 0, 0),
rgb(200, 200, 200)]
lVerts = [] -- создаем временный массив вершин
t = 0.0
dt = 1.0/nSeg
repeat with i = 1 to (nSeg + 1)
alfa = 0.0
dalfa = 2.0*PI/nDet
knot = calcKnot(t)
repeat with j = 1 to nDet -- заполнение вершин вокруг узла
knot_val = vector(cos(alfa)*r+knot.x,
knot.y, sin(alfa)*r+knot.z)
addat(lVerts, knot_val) -- добавляем вершину
alfa = alfa + dalfa
end repeat
t = t+dt
end repeat
res.vertexlist = lVerts
-- формирование списка треугольников
face_num = 1
repeat with i = 1 to nSeg
repeat with j = 1 to nDet -1
faceVerts = [(i)*nDet+j, (i-1)*nDet+j+1, (i-1)*nDet+j]
res.face[face_num].vertices = faceVerts
res.face[face_num].colors = [1, 2, 2]
faceVerts = [(i)*nDet+j+1, (i-1)*nDet+j+1, (i)*nDet+j]
res.face[face_num+1].vertices = faceVerts
res.face[face_num+1].colors = [1, 2, 2]
face_num = face_num + 2
end repeat
faceVerts = [(i-1)*nDet+1, (i-1)*nDet+nDet, (i)*nDet+nDet]
res.face[face_num].vertices = faceVerts
res.face[face_num].colors = [2, 2, 2]
faceVerts = [(i)*nDet+nDet, (i)*nDet+1, (i-1)*nDet+1]
res.face[face_num+1].vertices = faceVerts
res.face[face_num+1].colors = [1, 2, 2]
face_num = face_num + 2
end repeat
res.generatenormals(#smooth)
res.build()
obj = scene.newmodel("prut", res)
obj.transform.position = vector(0.0, -300.0, 0.0)
l_val = 0
delta_v = 6.0
end
on calcKnot(t) me -- вычисление координаты узла кривой
return p0*((1.0-t)*(1.0-t)*(1.0-t)) + 3.0*p1*t*((1.0-t)*(1.0-t)) + 3.0*p2*(1.0-t)*(t*t) + p3*(t*t*t)
end
on vect_length(point1, point2)
v = point1 - point2
return sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
end
on mouseDown me
mouseClick()
end
on calcMovie() me
if (l_val mod 20) = 0 then -- смена вектора движения направляющих точек
v2 = vector((random(2*delta_v) - delta_v)/4.0*delta_v,
(random(2*delta_v) - delta_v)/4.0*delta_v,
(random(2*delta_v) - delta_v)/4.0*delta_v)
v3 = vector((random(2*delta_v) - delta_v)/4.0*delta_v,
(random(2*delta_v) - delta_v)/4.0*delta_v,
(random(2*delta_v) - delta_v)/4.0*delta_v)
end if
-- вычисление новой позиции
p2 = p2+v2
p3 = p3+v3
knots = []
t = 0.0
dt = 1.0/nSeg
repeat with i = 1 to (nSeg + 1)
addat(knots, calcKnot(t))
t = t+dt
end repeat
repeat with i = 1 to (nSeg + 1)
if i = 1 then
n_vect = vector(0.0, 1.0, 0.0)
else
n_vect = (knots[i] - knots[i-1]) / vect_length(knots[i], knots[i-1])
end if
alfa = 0.0
dalfa = 2*PI/nDet
repeat with j = 1 to nDet
knot_val = vector(cos(alfa)*r, 0.0, sin(alfa)*r)
knot_val = knot_val - knot_val * n_vect + knots[i]
lVerts[(i-1)*nDet+j] = knot_val
alfa = alfa + dalfa
end repeat
end repeat
res.vertexlist = lVerts
res.generatenormals(#smooth)
res.build()
l_val = l_val + 1
end
on exitFrame me
mouseMove()
if the mouseDown then
camMovie()
end if
calcFrame()
calcMovie()
go to frame 1
end
Если Вы замените содержимое основного скрипта вот этим, то сможете созерцать фигуру чем-то смахивающую на шнурок, который стоит на одном своем конце и активно пытатся что-то найти другим. Я специально не буду описывать как это все работает. Рекомендую изучить пример самостоятельно, после того, как разберетесь в основах и потренеруетесь.
Кстати можете скачать готовый пример и поиграться с ним в директоре отсюда.
Источник: http://touch3d.net