В третьей части серии мы займемся созданием икосаэдров, их подразделением и превращением в сферу. Мы также рассмотрим два способа настройки затенения мешей (сглаживания).
Что такое икосаэдр?
Икосаэдр — это многогранник с 20 гранями. Существует несколько видов икосаэдров. Однако, чтобы создать икосферу, мы будем использовать только выпуклые регулярные икосаэдры (самый известный их вид). Вы можете найти больше информации о них и их свойствах в Википедии.
Итак, почему икосферы? Икосферы имеют более равномерное распределение геометрии, нежели UV-сферы. Деформирование UV-сфер часто дает странные результаты вблизи полюсов из-за более высокой плотности геометрии, в то время как икосферы дают более четкий и органический результат. Вдобавок к этому, икосферы асимметричны, что помогает создавать органическую деформацию.
Этот урок основан на оригинальном коде икосаэдра Андреаса Келера, адаптированном к Python 3 и Blender.
Настройка
Готов поспорить, вы уже знаете все это. Давайте начнем импорт, а затем перейдем к нашим обычным делам.
import bpy from math import sqrt # ----------------------------------------------------------------------------- # Settings name = 'Icosomething' scale = 1 subdiv = 5 # ----------------------------------------------------------------------------- # Add Object to Scene mesh = bpy.data.meshes.new(name) mesh.from_pydata(verts, [], faces) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) bpy.context.scene.objects.active = obj obj.select = True |
В разделе настроек переменная subdiv будет контролировать, сколько раз подразделять меш, а переменная scale будет простым параметром масштаба, как и в предыдущем уроке. Установка subdiv в значение 0 создаст икосаэдр (вместо икосферы). Обратите внимание, что значение subdiv равное 9 приведет к созданию меша с более чем 5 миллионами граней. Скорее всего вам нужно использовать значения ниже данного порога, конечно же, в зависимости от вашего оборудования.
Помещение сферы в икосферу
Простое разделение икосаэдра приведет нас только к более изысканному икосаэдру. Нам нужно убедиться, что вершины объединяются таким образом, который напоминает сферу.
Чтобы это произошло, мы должны убедиться, что вершины, которые мы добавляем, лежат на единичной сфере. Единичная сфера является «мнимой» сферой с радиусом 1. Мы можем определить положение каждой точки (вершины) на единичной сфере с помощью простой формулы, а затем зафиксировать ее координаты. Подробнее на Википедии.
Для этого у нас будет функция vertex(), которая фиксируется в единичной сфере (и также масштабируется).
def vertex(x, y, z): """ Return vertex coordinates fixed to the unit sphere """ length = sqrt(x**2 + y**2 + z**2) return [(i * scale) / length for i in (x,y,z)] |
Создаем базовый икосаэдр
Теперь, когда мы знаем, что вершины падают на единичную сферу, мы можем двигаться дальше и создавать базовый икосаэдр. Как и ранее с кубом, самым простым способом является ввод вершин и граней вручную.
Одним из способов построения икосаэдра является рассмотрение его вершин как углов трех ортогональных золотых плоскостей. Эти плоскости называются золотыми, потому что они следуют правилу золотого сечения. Вершины этих плоскостей лежат на координатах (0, ± 1, ± φ), (± φ, 0, ± 1) и (± 1, ± φ, 0). Заметим, что буква φ (phi) представляет значение золотого сечения, а ± означает «отрицательный или положительный».
Эти комбинации приводят к созданию 12 вершин, которые создают 20 равносторонних треугольников с 5 треугольниками, встречающимися в каждой вершине. Ознакомьтесь с приведенной ниже диаграммой.
Черт, это много математики! Однако, когда вы разбираетесь с этим, все становится довольно просто и прямолинейно (и повторяется). Вот код, который создает икосаэдр.
# -------------------------------------------------------------- # Make the base icosahedron # Golden ratio PHI = (1 + sqrt(5)) / 2 verts = [ vertex(-1, PHI, 0), vertex( 1, PHI, 0), vertex(-1, -PHI, 0), vertex( 1, -PHI, 0), vertex(0, -1, PHI), vertex(0, 1, PHI), vertex(0, -1, -PHI), vertex(0, 1, -PHI), vertex( PHI, 0, -1), vertex( PHI, 0, 1), vertex(-PHI, 0, -1), vertex(-PHI, 0, 1), ] faces = [ # 5 faces around point 0 [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], # Adjacent faces [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], # 5 faces around 3 [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], # Adjacent faces [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1], ] |
Стратегия для подразделения
Мы можем взять треугольник и разделить каждое ребро, создав на его месте три треугольника. По сути, треугольники превращаются в маленькие трифорсы. Обратите внимание, что когда я говорю «разделить», я не говорю о фактическом запуске оператора и разделении ребра. Под этим я подразумеваю создание новых вершин по середине каждого ребра и создании трех новых граней.
Однако, если бы мы обходили все ребра и разделяли их, мы бы быстро столкнулись с теми же ребрами, которые мы уже разделили ранее. Это приведет к большому количеству дубликатов вершин и головной боли при попытке создания граней.
Чтобы этого не произошло, давайте сохраним список ребер, которые мы уже разделили (кеш), и проверим их перед очередным разделением. Этот кеш будет словарем. Ключами будут индекс вершин, упорядоченные от меньшего к большему. Таким образом, ключ останется прежним, независимо от того, как мы перебираем вершины ребра.
middle_point_cache = {} def middle_point(point_1, point_2): """ Find a middle point and project to the unit sphere """ # We check if we have already cut this edge first # to avoid duplicated verts smaller_index = min(point_1, point_2) greater_index = max(point_1, point_2) key = '{0}-{1}'.format(smaller_index, greater_index) if key in middle_point_cache: return middle_point_cache[key] # If it's not in cache, then we can cut it vert_1 = verts[point_1] vert_2 = verts[point_2] middle = [sum(i)/2 for i in zip(vert_1, vert_2)] verts.append(vertex(*middle)) index = len(verts) - 1 middle_point_cache[key] = index return index |
Средняя вершина вычисляется путем добавления координат обеих вершин и деления их на 2. Наконец, мы добавляем ее в кэш и возвращаем индекс, чтобы сделать список граней.
Подразделение
С помощью функции middle_point() мы можем перейти к циклу и созданию подразделений.
На каждом уровне подразделений мы создаем новый пустой список граней, а в конце мы заменяем исходный список граней новым. Затем мы проходим через каждую грань, находим среднюю точку для трех ребер, сохраняем индексы и создаем из них 4 новые грани (помните диаграмму выше).
# Subdivisions # -------------------------------------------------------------- for i in range(subdiv): faces_subdiv = [] for tri in faces: v1 = middle_point(tri[0], tri[1]) v2 = middle_point(tri[1], tri[2]) v3 = middle_point(tri[2], tri[0]) faces_subdiv.append([tri[0], v1, v3]) faces_subdiv.append([tri[1], v2, v1]) faces_subdiv.append([tri[2], v3, v2]) faces_subdiv.append([v1, v2, v3]) faces = faces_subdiv |
Сделаем сферу гладкой
Теперь у нас есть меш, который довольно хорошо аппроксимирует сферу, но все еще выглядит ступенчато. Время сгладить его.
Smooth shading — это атрибут граней. Таким образом, чтобы сгладить весь меш, нам нужно включить гладкое затенение для всех его граней. Вы можете сделать это, используя тот же оператор, который мы используем, когда нажимаем кнопку Smooth на панели инструментов:
bpy.ops.object.shade_smooth() |
Это будет прекрасно работать для этого скрипта, потому что контекст подходит для оператора. В других случаях вы можете обнаружить, что оператор отказывается работать из-за «неправильного контекста (incorrect context)». Контекст в Blender — это своего рода божественная переменная, которая содержит информацию о текущем состоянии приложения. Это включает в себя такие вещи, как положение курсора мыши, активная область и многое другое. Вы можете переопределить контекст при вызове оператора, но в настоящее время нет простого способа узнать, что каждый оператор хочет увидеть в качестве контекста.
К счастью, есть еще один способ сделать это «низкоуровневым» способом, установив сглаживание для каждой грани в цикле.
for face in mesh.polygons: face.use_smooth = True |
В мире скриптов Blender «Низкий уровень» относится к пропуску операторов и прямому доступу к методам и атрибутам объектов. from_pydata() — еще один пример работы на низком уровне.
Преимущества низкоуровневого заключается в том, что он не зависит от контекста, он часто более гибкий и экономит накладные расходы на систему операторов. В этом случае вы также можете применять сглаживание только для некоторых граней.
Финальный код
import bpy from math import sqrt # ----------------------------------------------------------------------------- # Settings scale = 1 subdiv = 5 name = 'Icosomething' # ----------------------------------------------------------------------------- # Functions middle_point_cache = {} def vertex(x, y, z): """ Return vertex coordinates fixed to the unit sphere """ length = sqrt(x**2 + y**2 + z**2) return [(i * scale) / length for i in (x,y,z)] def middle_point(point_1, point_2): """ Find a middle point and project to the unit sphere """ # We check if we have already cut this edge first # to avoid duplicated verts smaller_index = min(point_1, point_2) greater_index = max(point_1, point_2) key = '{0}-{1}'.format(smaller_index, greater_index) if key in middle_point_cache: return middle_point_cache[key] # If it's not in cache, then we can cut it vert_1 = verts[point_1] vert_2 = verts[point_2] middle = [sum(i)/2 for i in zip(vert_1, vert_2)] verts.append(vertex(*middle)) index = len(verts) - 1 middle_point_cache[key] = index return index # ----------------------------------------------------------------------------- # Make the base icosahedron # Golden ratio PHI = (1 + sqrt(5)) / 2 verts = [ vertex(-1, PHI, 0), vertex( 1, PHI, 0), vertex(-1, -PHI, 0), vertex( 1, -PHI, 0), vertex(0, -1, PHI), vertex(0, 1, PHI), vertex(0, -1, -PHI), vertex(0, 1, -PHI), vertex( PHI, 0, -1), vertex( PHI, 0, 1), vertex(-PHI, 0, -1), vertex(-PHI, 0, 1), ] faces = [ # 5 faces around point 0 [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], # Adjacent faces [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], # 5 faces around 3 [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], # Adjacent faces [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1], ] # ----------------------------------------------------------------------------- # Subdivisions for i in range(subdiv): faces_subdiv = [] for tri in faces: v1 = middle_point(tri[0], tri[1]) v2 = middle_point(tri[1], tri[2]) v3 = middle_point(tri[2], tri[0]) faces_subdiv.append([tri[0], v1, v3]) faces_subdiv.append([tri[1], v2, v1]) faces_subdiv.append([tri[2], v3, v2]) faces_subdiv.append([v1, v2, v3]) faces = faces_subdiv # ----------------------------------------------------------------------------- # Add Object to Scene mesh = bpy.data.meshes.new(name) mesh.from_pydata(verts, [], faces) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) bpy.context.scene.objects.active = obj obj.select = True # ----------------------------------------------------------------------------- # Smoothing #bpy.ops.object.shade_smooth() for face in mesh.polygons: face.use_smooth = True |
Завершение
На этом завершается третья часть этой серии. Если вы заинтересованы в создании сфер или преобразовании точек/объектов к сферическим формам, почитайте больше о единичной сфере и том, как применять ее вдоль нормалей.
Вещи, которые вы можете сделать для себя:
- Оптимизируйте код (подсказка: вам не нужно хранить ключ в виде строки)
- Примените матрицы вращения и перемещения из предыдущей части
- Удалите переменную масштаба и вместо этого используйте матрицу
В следующий раз мы вернемся к кубам и узнаем, как сделать скругленный куб, применить модификаторы и многое другое.
Ну где вы нытики?! Где все те люди, которые говорили мне, что урок про чашку или моушин-графику не нужен? Там десятки комментариев и результатов работ. Тут прошли сутки и ни одного! Покажите мне, как вы оптимизировали код. Или как вы повернули сферу? Что нибудь…
Обращаюсь ко всем «[i]профессионалам[/i]» компьютерной графики. Не учите меня, что мне выкладывать на своем сайте. Я знаю кто на что способен и всегда буду делать уроки как для новичков (чашка), так и для тех, кто выше в пищевой цепочке (как этот). И вашим и нашим.
Всем добра! 🈸
Артем, ну не шуми, дай вчитаться и переварить. Норм урок.
Та все ок, это я не мог просто пройти мимо и не позлорадствовать. Ведь реально бесят ❎ Сами нормаль вывернуть не могут и учат тут меня, что мне делать, а что нет.
Видать сильно достали… Держись Артем!! )))
Без обид, но ты реально «их» обухом по голове по самую задницу, я с Python вроде знаком (раньше немного работал), но не решился бы что то вообще коментить без знания Blender API. Ранее я тебя спрашивал, годны ли книги для более ранних версий, новых на русском нет. Я бы начал все таки с ссылочи типа этой : https://ru.wikibooks.org/wiki/Blender_%D0%B4%D0%BB%D1%8F_%D0%BD%D0%B0%D1%87%D0%B8%D0%BD%D0%B0%D1%8E%D1%89%D0%B8%D1%85/Python
Да и урок для «масс» сложноват будет. Урок нужный, но и не для уровня «нажмите shift-a/add — mash/Cube»
Народ «не шарит» но поддерживает твое [b]правое дело[/b]. Вроде советы не любишь»что делать и что выкладывать», но ты реально напугал уроком тех «кто нормаль повернуть не могут», потому и молчание. Вопрос то и в том «А это им надо?». Сам собираюсь освоить Blender Python, но «умничать» на эту тему даже не решусь, может быть в бууудууущееем, когда что то сам сделаю. И то, меня интересует тема создания своих аддонов, а моделинг с помощью Python, это лучше в FreeCad поупражняться, там половина функций через Python скрипты работает.
P.S Удачи, так держать и не обламываться.🈸
Здесь забористый матан с вышки, это не кнопки жмакать мышкой.
Многие просто это не понимают, и пропускают статью мимо :)
Узконацеленная статья, расчитанная больше на программистов, а не новичков «моделлеров» которых здесь подавляющее большинство.
это ж просто субдив и сетка… (
Вот результат, правда не в блендере и не на пайтоне, а на шарпе и в Unity 🚺
MeshBuilder.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MeshBuilder : MonoBehaviour {
private Transform MeshTransform;
public Transform meshTransform
{
get
{
return MeshTransform;
}
set
{
MeshTransform = value;
}
}
private List Vertices = new List (); vertices
public List
{
get
{
return Vertices;
}
}
private List Normals = new List (); normals
public List
{
get
{
return Normals;
}
}
private List UVs = new List (); uvs
public List
{
get
{
return UVs;
}
}
private List Indices = new List(); indices
public List
{
get
{
return Indices;
}
set
{
Indices = value;
}
}
public void AddTriangle(int index0, int index1, int index2)
{
int[] temp = {index0, index1, index2};
Indices.Add(temp);
}
public Mesh CreateMesh()
{
Mesh mesh = new Mesh();
mesh.vertices = Vertices.ToArray(); _edgeCache = new Dictionary();
int[] tempTri = new int[Indices.Count *3];
for(int i = 0; i < Indices.Count;i++) { tempTri[(i * 3)] = Indices[i][0]; tempTri[(i * 3)+1] = Indices[i][1]; tempTri[(i * 3)+2] = Indices[i][2]; } mesh.triangles = tempTri; if (Normals.Count == Vertices.Count) mesh.normals = Normals.ToArray(); if (UVs.Count == Vertices.Count) mesh.uv = UVs.ToArray(); else Debug.Log("UV Error"); mesh.RecalculateBounds(); mesh.RecalculateNormals(); return mesh; } } Icosider.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class Icosider : MonoBehaviour { MeshBuilder meshBuilder = new MeshBuilder(); Dictionary
public int subdiv = 3;
public bool normilize = true;
public float scale = 2;
// Use this for initialization
void Start () {
float phi = (1 + Mathf.Sqrt(5)) /2;
meshBuilder.vertices.Add(sphereVertex(new Vector3(-1, phi,0),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(1, phi,0),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(-1,-phi,0),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(1, -phi,0),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(0,-1, phi),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(0, 1, phi),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(0,-1,-phi),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(0, 1,-phi),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(phi, 0, -1),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(phi, 0, 1),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(-phi, 0, -1),scale));
meshBuilder.vertices.Add(sphereVertex(new Vector3(-phi, 0, 1),scale));
meshBuilder.AddTriangle(0,11,5);
meshBuilder.AddTriangle(0,5,1);
meshBuilder.AddTriangle(0,1,7);
meshBuilder.AddTriangle(0,7,10);
meshBuilder.AddTriangle(0,10,11);
meshBuilder.AddTriangle(1,5,9);
meshBuilder.AddTriangle(5,11,4);
meshBuilder.AddTriangle(11,10,2);
meshBuilder.AddTriangle(10,7,6);
meshBuilder.AddTriangle(7,1,8);
meshBuilder.AddTriangle(3,9,4);
meshBuilder.AddTriangle(3,4,2);
meshBuilder.AddTriangle(3,2,6);
meshBuilder.AddTriangle(3,6,8);
meshBuilder.AddTriangle(3,8,9);
meshBuilder.AddTriangle(4,9,5);
meshBuilder.AddTriangle(2,4,11);
meshBuilder.AddTriangle(6,2,10);
meshBuilder.AddTriangle(8,6,7);
meshBuilder.AddTriangle(9,8,1);
Subdiv(subdiv);
MeshFilter filter = GetComponent();
if(filter != null)
{
filter.sharedMesh = meshBuilder.CreateMesh();
}
}
Vector3 sphereVertex(Vector3 vert,float scale)
{
float length = Mathf.Sqrt(Mathf.Pow(vert.x,2) + Mathf.Pow(vert.y,2) + Mathf.Pow(vert.z,2));
Debug.Log( «Length » + (Mathf.Pow(vert.x/length,2) + Mathf.Pow(vert.y/length,2) + Mathf.Pow(vert.z/length,2)));
Vector3 result = new Vector3(vert.x * scale, vert.y * scale, vert.z*scale);
if(normilize == true)
result = new Vector3((vert.x * scale) / length, (vert.y * scale) / length, (vert.z * scale) / length);
return result;
}
int MiddlePoint(int point1, int point2)
{
int smallerIndex = Mathf.Min(point1, point2);
int graterIndex = Mathf.Max(point1,point2);
string key = smallerIndex + «-» + graterIndex;
if(_edgeCache.ContainsKey(key))
{
return _edgeCache[key];
}
Vector3 vert1 = meshBuilder.vertices[point1];
Vector3 vert2 = meshBuilder.vertices[point2];
Vector3 middle = (vert1 + vert2) / 2;
meshBuilder.vertices.Add(sphereVertex(middle,scale));
int index = meshBuilder.vertices.Count-1;
_edgeCache[key] = index;
return index;
}
void Subdiv(int subdiv) facesSubdiv = new List();
{
for(int i = 0; i < subdiv;i++) { List
foreach(int[] tri in meshBuilder.indices)
{
int v1 = MiddlePoint(tri[0],tri[1]);
int v2 = MiddlePoint(tri[1],tri[2]);
int v3 = MiddlePoint(tri[2],tri[0]);
facesSubdiv.Add(new int[]{tri[0], v1, v3});
facesSubdiv.Add(new int[]{tri[1], v2, v1});
facesSubdiv.Add(new int[]{tri[2], v3, v2});
facesSubdiv.Add(new int[]{v1, v2, v3});
}
meshBuilder.indices = facesSubdiv;
}
}
}
На будущее, здесь в редакторе сообщений есть тег для кода. Он находится в правом нижнем углу, если нажать карандаш и выбрать угловые скобки — это тег «code». В нём код будет более читабельный, с подсветкой и отступами:
[code]
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyClass : MonoBehaviour
{
private void Start() {
Debug.Log(«Hello, World»);
}
}
[/code]
Спасибо большое! Просто офигительная серия уроков)
Здесь забористый матан с вышки, это не кнопки жмакать мышкой.
Многие просто это не понимают, и пропускают статью мимо :)
Узконацеленная статья, расчитанная больше на программистов, а не новичков «моделлеров» которых здесь подавляющее большинство.
Спасибо большое! Просто офигительная серия уроков)
Ну где вы нытики?! Где все те люди, которые говорили мне, что урок про чашку или моушин-графику не нужен? Там десятки комментариев и результатов работ. Тут прошли сутки и ни одного! Покажите мне, как вы оптимизировали код. Или как вы повернули сферу? Что нибудь…
Обращаюсь ко всем «[i]профессионалам[/i]» компьютерной графики. Не учите меня, что мне выкладывать на своем сайте. Я знаю кто на что способен и всегда буду делать уроки как для новичков (чашка), так и для тех, кто выше в пищевой цепочке (как этот). И вашим и нашим.
Всем добра! 🈸
Привет. Скажите пожалуйста, есть ли возможность в Blender программировать поведение «света» (распространение лучей, возможно, их количество, отражение от объектов)? Если это возможно, было бы круто увидеть статью на эту тему.
Во всех уроках с нодами мы «программируем» поведение лучей света. Их количество, количество их отражений… все, или практически все.