Мы займемся созданием икосаэдров, их подразделением и превращением в
сферу. Мы также рассмотрим два способа настройки затенения мешей (сглаживания).
Что такое икосаэдр?
Икосаэдр – это многогранник с 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
Завершение
На этом завершается третья часть этой серии. Если вы
заинтересованы в создании сфер или преобразовании точек/объектов к сферическим
формам, почитайте больше о единичной сфере и том, как применять ее вдоль
нормалей.
Вещи, которые вы можете сделать для себя:
- Оптимизируйте код (подсказка:
вам не нужно хранить ключ в виде строки)
- Примените матрицы вращения и
перемещения из предыдущей части
- Удалите переменную масштаба и
вместо этого используйте матрицу
Что такое икосаэдр?
Икосаэдр – это многогранник с 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 |
Завершение
На этом завершается третья часть этой серии. Если вы
заинтересованы в создании сфер или преобразовании точек/объектов к сферическим
формам, почитайте больше о единичной сфере и том, как применять ее вдоль
нормалей.
Вещи, которые вы можете сделать для себя:
- Оптимизируйте код (подсказка:
вам не нужно хранить ключ в виде строки)
- Примените матрицы вращения и
перемещения из предыдущей части
- Удалите переменную масштаба и
вместо этого используйте матрицу
Комментариев нет:
Отправить комментарий