Как из SCNSphere сделать что угодно с помощью Metal. Например, 3D RGB-Cube

Джулс: Знаешь почему так? 
Брэд: Метрическая система?
Джулс: Гляди как наш Бред мозговит!
Смышленый сукин сын, это точно — всё просек.
(из фильма «Криминальное чтиво»)

Дорогие слушатели нашей радиостанции, если вы переодически следите за нашими выпусками, то в целом в курсе наших новейших исследований цвета в нашей высоко-профессиональной любительской лаборатории. И уже имеете представление о том как цвет влияет на общее восприятие изображения. Очень важной фишкой в таких исследованиях являются цветовые пространства, в которых, обычно, можно производить манипуляции с пикселами картинки, системно её «ухудшая». Так удачно сложилось, что почти все такие пространства 3-х компонентные. А это означает, что мы можем попытаться визуализировать то как наши дурные наклонности могут его деформировать относительно исходного состояния в 3D сцену.

В многообразии фреймворков Apple есть очень мощный SDK SceneKit.  Сегодня мы распотрошим сразу двух кроликов: изучим как нам нарисовать произвольную 3D-фигуру с помощью инструментов SceneKit. А так же научимся добавлять очень большое количество дополнительных объектов в 3D-сцену, быстро её  деформировать, модифицировать и манипулировать свойствами всех добавленных объектов с помощью Metal.

А заодно докажем, что серая сфера на самом деле это цветной куб.

sphere-deformation
Рис.1. Лёгким движением руки сфера превращается в элегантный кубик

 

Сразу Код(да)

Продемонстрировать эти наши доказательства мы намереваемся с помощью приложения использующего SceneKit и Metal, а так же наш новый блестящий примус: проект IMProcessing. Как всегда, если читать много букв лень, лезем в ImageMetalling и клонируем пример ImageMetalling-15. Дальше традиционные pod install и сборка.

SceneKit

SceneKit — высокоуровневый фреймворк, предназначенный для работы над композициями трехмерных сцен. Он включает в себя движок расчета физики, генератор частиц и удобный API. В основе SceneKit лежит граф сцены. Это все очень похоже на  то, что широко используется в игровых движках типа Unity и 3D-редакторах типа Blender (а это всё вообще-то очень круто).

Основным элементом графа сцены, в терминах SceneKit,  является узел SCNNode. Он содержащий в себе информацию о положении, углах поворота, масштабе, а так же несколько свойств описывающих «физическую» сущность объекта. Положение, вращение и масштаб дочерних узлов определяются относительно родительского узла. Корневым элементом для создания любой сцены является SCNScene. Рисуется всё это в SCNView.

Свойства узла могут задавать:

  • SCNGeometry — определяет форму объекта и его отображение за счет набора материалов;
  • SCNCamera — отвечает за точку пространства сцены, из которой мы видим сцену;
  • SCNLight — отвечает за освещение, его положение влияет на все объекты сцены.

Очевидно, для создания любого 3D изображения в целом необходимо и достаточно умелых пальцев рук, представления о геометрии и линейной алгебре, терпения и усидчивости. И немного-много свободного времени.

 

Задача

В одном из постов мы уже разобрали как можно, достаточно быстро, написать свой собственный Инстаграм используя движок Metal. В нём мы достаточно подробно разобрали, что такое LUT-ы, зачем LUT-ы и как их использовать. Но, одно дело понимать для чего и как, а другое дело зафиксировать понимание на сетчатке глаза — люди же визуалы.

И так, будем формулировать задачу в таком ключе: возьмем исходное цветовое RGB-пространство представим его в координатах xyz. Так же мы знаем, что для наших вычислений оно всегда ограничено размерностью [0,1] по всем осям. Если мы нарисуем такой  объект то получим кубик — что-то похожее на то, как это показано на Рис.1. Назовем эту фигуру исходным LUT (identity) — каждая дискретная точка пространства отображается сама в себя. Как мы выяснили ранее нам интересны цвета, по каким-то правилам деформировать — т.е. мы можем показать с помощью такого преобразования кубика общую деформацию исходного пространства в новое. И эта новая форма, по идее нам о чём-то важном в итоге скажет. Точнее покажет.

На Рис.2 видно как искажается исходное RGB-пространство с помощью фильтров использующих LUT-ы из файлов filter2.cube и filter3.cube. Один из фильтров вообще сжимает все пространство в «прямую» (но с разной плотностью семплов!). И похожее преобразование цветов делается при фиксации изображения с помощью галогенидов серебра.

lut-examples-metalagram
Рис 2. То как выглядят LUT-ы на самом деле

«Цветовые» искажения отобразят кубик в какую-то другую фигуру — отразят исходные точки в новые.

 

Создание сцены

Ну, окей, про очевидные вещи вроде понятно. Как теперь все это нарисовать? Начнем с того, что мы создадим сцену из трех типов фигур:

  1. Сферы нам нужны для отрисовки крайних точек куба и сетки из «частиц» для отрисовки внутренностей кубика. Для того, что бы видеть искажения тела и представлять «ландшафт» этой деформации;
  2. Цилиндры нам нужны для отрисовки ребер куба;
  3. Бокс нам нужен для отрисовки граней.

Все эти объекты рендеринга уже подготовлены в SceneKit в виде полигональной сетки, т.е. с заданными вершинами и треугольниками для отрисовки свойств поверхностей этих объектов. Вроде бы все просто, но есть одна хрень, которая несколько усложняет задачу: для правильной отрисовки деформации пространства и частиц должно быть много, и поверхность бокса должна быть детализирована достаточно точно — т.е. должно быть задано много вершин описывающих треугольники. А еще нам нужно нарисовать много частиц. Например, частиц на рисунках 16x16x16, т.е. 4096, а mesh бокса может быть детализирован до такого-же разрешения для каждой грани. Т.е. все хорошо — мы нарисовали сцену с помощью SceneKit и она даже отрендерилась на GPU. Но как её модифицировать? Т.е. изменять свойства на CPU? Ну, очевидно нам опять дорога в  шейдеры и ядра на MSL — т.е. там все и будем перевычислять;)

/// Программируем рендеринг материала в GPU на Metal
let program:SCNProgram = {
let p = SCNProgram()
p.vertexFunctionName = "projectionVertex"
p.fragmentFunctionName = "materialFragment"
p.isOpaque = false;
return p
}()
/// Создаем материал который будем рендерить в шейдерах Metal
lazy var material:SCNMaterial = {
let m = SCNMaterial()
if self.renderCube {
m.program = self.program
}
return m
}()
///
/// Задаем геометрию "монолитного" RGB-куба
///
lazy var meshGeometry:SCNGeometry = {
///
/// В качестве исходной фигуры можно было бы задать объект кубической формы.
/// Но он содержит по умолчанию всего 8 сегментов, что мало для визуально-различимого
/// рендеринга "дефектов" LUT. Для этого нам придется "накидать" на каждую сторону побольше разрешения:
///
/// let g = SCNBox(width: 2, height: 2, length: 2, chamferRadius: 0)
/// let segments = 64
/// g.widthSegmentCount = segments
/// g.heightSegmentCount = segments
/// g.lengthSegmentCount = segments
///
/// Но мы выбираем сферу как более детализорованно-собранный mesh с более простой формой настройки:)
/// А деформировать нам по сути все равно какой объект.
///
let g = SCNSphere(radius: 2);
/// Повышаем детализацию "монолита"
g.segmentCount = 128
///
/// Определяем материал геометрии, который будем рендерить и заодно деформировать в программе на MSL
///
g.materials = [self.material]
return g
}()
view raw SCNProgram.swift hosted with ❤ by GitHub

 

Рендеринг сферы в куб и раскраска в GPU

В листинге кусочка программы мы показали как привязать шейдеры для Metal Shading Language (MSL) к сцене для того, что-бы созданный для SceneKit объект можно было кастомно модифицировать в наших практических целях. А еще мы решили, что в пень кубики — юзаем шарики. Хотя почти никакой разницы в целом нет. Просто сфера более экономически выгодная структура — ибо 64-х сегментов нам хватит для построения более-мене читаемого изображения любой формы. Hу, еще, да — нам нужно раскрасить поверхность ~ правильными цветами и показать, что произошло внутри с точками пространства — т.е. сделать грани прозрачными.

using namespace metal;
#include <SceneKit/scn_metal>
// Стандартные параметры модели (узла)
typedef struct {
float4x4 modelTransform;
float4x4 modelViewTransform;
float4x4 normalTransform;
float4x4 modelViewProjectionTransform;
} NodeBuffer;
// Стандартные параметры вершины
typedef struct {
float3 position [[ attribute(SCNVertexSemanticPosition) ]];
float3 normal [[ attribute(SCNVertexSemanticNormal) ]];
} VertexInput;
// Результат вершинного шейдера
typedef struct {
float4 position [[position]];
float2 texCoords;
float3 rgb;
float3 surfaceColor;
} VertexOutput;
// Основной вершинный шейдер программы
vertex VertexOutput projectionVertex(VertexInput in [[ stage_in ]],
texture3d<float, access::sample> lut3d [[texture(0)]],
constant SCNSceneBuffer &scn_frame [[buffer(0)]],
constant NodeBuffer &scn_node [[buffer(1)]]
)
{
VertexOutput vert;
// конвертируем координаты [-1:1] в представление цветов: [0:1]
vert.rgb = (in.position+1) * 0.5;
// прикладываем LUT
vert.rgb = lut3d.sample(IMProcessing::lutSampler, vert.rgb).rgb;
// вычисляем новую позицию вершины
float3 pos = (vert.rgb - 0.5) * 2;
// позиционируем в соответствии с проекцией
vert.position = scn_node.modelViewProjectionTransform * float4(pos, 1.0);
return vert;
}
// Фрагментный шейдер программы
fragment float4 materialFragment(VertexOutput in [[stage_in]])
{
// текущий семпл
return float4(in.rgb, 0.6);
}
view raw IMProcessing.metal hosted with ❤ by GitHub

Что делать с 4096-ю частицами?

В целом же это тоже до хрена. Да еще же нужно все это интерполировать из уже готового LUT. А мы делали это только для коррекции текстур изображения. Да в целом то тоже самое делать — просто передаем в ядро не текстуру, а исходный набор 4096 точек с rgb позициями и точно также семплируем их в новые позиции только в MTLBuffer для чтения потом в контексте CPU.

constexpr sampler lutSampler(address::clamp_to_edge, filter::linear, coord::normalized);
kernel void kernel_clutColorMapper(
texture3d<float, access::sample> clut [[texture(0)]],
device float3 *reference [[buffer(0)]],
device float3 *target [[buffer(1)]],
uint gid [[thread_position_in_grid]]
)
{
float3 rgb = reference[gid];
target[gid] = clut.sample(lutSampler, rgb).rgb;
}
view raw IMProcessing.metal hosted with ❤ by GitHub

Для того, что-бы не перерисовывать свойства частиц в сцене при каждом движении задаем для SCNView делегат кастомного рендеринга.

///
/// Говорим делегату рендеренга, что на новом шаге изменяем позции узловых точек RGB-куба в сцене
/// Одновременно с этим на GPU шейдер деформирует координаты вершин нашего монолитного RGB-куба
///
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard isChanged else { return }
for (i,rgb) in colors.enumerated() /*meshGrid.enumerated()*/ {
let p = meshGrid[i]
let rgba = float4(rgb.r,rgb.g,rgb.b,1)
p.color = NSColor(color: rgba)
}
self.isChanged = false
}
view raw IMProcessing.swift hosted with ❤ by GitHub

 

Выводы

Друзья, считаю, что сегодня был настоящий, хоть и без затей, джорней в мир 3D-графики и ускорителей в их изначальном смысле. Мы увидели, что не все то куб, что кажется пеньком. А так же научились использовать SceneKit совместно с Metal, а это как минимум забавно и скрашивает нашу серую реальность виртуальными развлечениями. Вот много вы знаете людей которым луты интересно рисовать и об этом рассказывать? … Вот и я тоже.

Всем инжой!

 


Еще раз напомню: авторы блога не преследуют задачи быть предельно корректным, но если заметили явную ашипку, если написали явную глупость, если что-то не понятно: комментируйте или пишите на: imagemetalling [*] gmail.com .

Как из SCNSphere сделать что угодно с помощью Metal. Например, 3D RGB-Cube: Один комментарий

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s