- PVSM.RU - https://www.pvsm.ru -
Привет! Мой сегодняшний пост по программированию графики будет не таким объемным, как предыдущие. Почти в любом сложном деле иногда есть место несерьезному, и сегодня мы будем рендерить котиков. Точнее я хочу рассказать о реализации алгоритма рендеринга меха Shells and Fins (SAF) традиционно для Direct3D 11 и OpenGL 4. За подробностями прошу под кат.
Алгоритм рендеринга меха SAF, как нетрудно догадаться из названия, состоит из 2 частей: рендеринг «панцирей» (shells) и рендеринг «плавников» (fins). Возможно, некоторым покажутся забавными эти наименования, но они в полной отражают то, что создается алгоритмом для создания иллюзии ворсистой поверхности. Подробнее с реализацией алгоритма для Direct3D 10 можно ознакомиться в статье и демке NVidia [1], мою демку для Direct3D 11 и OpenGL 4 можно найти здесь [2]. Проект называется Demo_Fur. Для сборки вам понадобятся Visual Studio 2012/2013 и CMake [3].
Мех состоит из огромного количества волосков, нарисовать каждый из которых в отдельности на текущий момент не представляется возможным в реальном времени, хотя определённые попытки [4] были у NVidia. Для того, чтобы создать иллюзию ворсистой поверхности применяется технология, чем-то напоминающая воксельный рендеринг. Заготавливается трёхмерная текстура, представляющая собой небольшой участок меховой поверхности. Каждый воксель в ней определяет вероятность прохождения ворсинок через себя, что с графической точки зрения определяет значение прозрачности в той или иной точке при рендеринге. Такую трехмерную текстуру можно сгенерировать (один из способов описан здесь [5]). Возникает закономерный вопрос, как эту текстуру рендерить. Для этого вокруг геометрии рисуются «панцири», т.е. копии исходной геометрии, формируемые путем масштабирования этой геометрии на небольшие значения. Получается своеобразная матрешка, на каждый слой которой накладывается слой из трехмерной текстуры меха. Слои рисуются последовательно с включенным альфа-блендингом, что в результате дает некоторую иллюзию ворсистости. Однако, этого не достаточно, чтобы материал напоминал мех. Для достижения цели необходимо выбрать правильную модель освещения.
Мех относится к категории ярко выраженных анизотропных материалов. Классические модели освещения (например, модель Блинна-Фонга [6]) рассматривают поверхности как изотропные, т.е. свойства поверхности не зависят от ее ориентации. На практике это означает, что при повороте плоскости вокруг ее нормали характер освещения не меняется. Модели освещения такого класса для вычисления затененности используют величину угла между нормалью и направлением падения света. Анизотропные модели освещения используют тангенты (вектора перпендикулярные нормалям, которые вместе с нормалями и бинормалями образуют базис) для вычисления освещенности. Подробнее про анизотропное освещение можно прочитать здесь [7].
Анизотропное освещение вычисляется отдельно для каждого слоя меха. Значения тангента в той или иной точке поверхности определяется при помощи карты тангентов. Карта тангентов формируется практически так же как широко известная карта нормалей [8]. В случае текстуры меха вектором тангента будет являться нормализованное направление ворсинки. Таким образом, трёхмерная текстура меха будет содержать 4 канала. В RGB будет храниться упакованный вектор тангента, в альфа-канале будет содержаться вероятность прохождения ворсинки через эту точку. Добавим к этому учет самозатенения меха и получим достаточно реалистично выглядящий материал.
Иллюзия будет нарушена, если человек внимательно посмотрит на внешние рёбра объекта. При определенных углах между гранями в ситуации, когда одна грань видна, а другая нет, слои меха могут быть невидимы для наблюдателя. Во избежание этой ситуации на таких рёбрах формируется дополнительная геометрия, которая вытягивается вдоль их нормалей. Результат несколько напоминает плавники у рыб, что и обусловило вторую часть названия алгоритма.
Реализации на обоих API в целом идентична, отличаются лишь незначительные детали. Рендеринг будем производить по следующей схеме:
#include <common.h.hlsl>
struct GS_INPUT
{
float4 position : SV_POSITION;
float2 uv0 : TEXCOORD0;
float3 normal : TEXCOORD1;
};
struct GS_OUTPUT
{
float4 position : SV_POSITION;
float3 uv0 : TEXCOORD0;
};
texture2D furLengthMap : register(t0);
SamplerState defaultSampler : register(s0);
[maxvertexcount(6)]
void main(lineadj GS_INPUT pnt[4], inout TriangleStream<GS_OUTPUT> triStream)
{
float3 c1 = (pnt[0].position.xyz + pnt[1].position.xyz + pnt[2].position.xyz) / 3.0f;
float3 c2 = (pnt[1].position.xyz + pnt[2].position.xyz + pnt[3].position.xyz) / 3.0f;
float3 viewDirection1 = -normalize(viewPosition - c1);
float3 viewDirection2 = -normalize(viewPosition - c2);
float3 n1 = normalize(cross(pnt[0].position.xyz - pnt[1].position.xyz, pnt[2].position.xyz - pnt[1].position.xyz));
float3 n2 = normalize(cross(pnt[1].position.xyz - pnt[2].position.xyz, pnt[3].position.xyz - pnt[2].position.xyz));
float edge = dot(n1, viewDirection1) * dot(n2, viewDirection2);
float furLen = furLengthMap.SampleLevel(defaultSampler, pnt[1].uv0, 0).r * FUR_LENGTH;
if (edge > 0 && furLen > 1e-3)
{
GS_OUTPUT p[4];
p[0].position = mul(pnt[1].position, modelViewProjection);
p[0].uv0 = float3(pnt[1].uv0, 0);
p[1].position = mul(pnt[2].position, modelViewProjection);
p[1].uv0 = float3(pnt[2].uv0, 0);
p[2].position = mul(float4(pnt[1].position.xyz + pnt[1].normal * furLen, 1), modelViewProjection);
p[2].uv0 = float3(pnt[1].uv0, 1);
p[3].position = mul(float4(pnt[2].position.xyz + pnt[2].normal * furLen, 1), modelViewProjection);
p[3].uv0 = float3(pnt[2].uv0, 1);
triStream.Append(p[2]);
triStream.Append(p[1]);
triStream.Append(p[0]);
triStream.RestartStrip();
triStream.Append(p[1]);
triStream.Append(p[2]);
triStream.Append(p[3]);
triStream.RestartStrip();
}
}
#version 430 core
layout(lines_adjacency) in;
layout(triangle_strip, max_vertices = 6) out;
in VS_OUTPUT
{
vec2 uv0;
vec3 normal;
} gsinput[];
out vec3 texcoords;
const float FUR_LAYERS = 16.0f;
const float FUR_LENGTH = 0.03f;
uniform mat4 modelViewProjectionMatrix;
uniform sampler2D furLengthMap;
uniform vec3 viewPosition;
void main()
{
vec3 c1 = (gl_in[0].gl_Position.xyz + gl_in[1].gl_Position.xyz + gl_in[2].gl_Position.xyz) / 3.0f;
vec3 c2 = (gl_in[1].gl_Position.xyz + gl_in[2].gl_Position.xyz + gl_in[3].gl_Position.xyz) / 3.0f;
vec3 viewDirection1 = -normalize(viewPosition - c1);
vec3 viewDirection2 = -normalize(viewPosition - c2);
vec3 n1 = normalize(cross(gl_in[0].gl_Position.xyz - gl_in[1].gl_Position.xyz, gl_in[2].gl_Position.xyz - gl_in[1].gl_Position.xyz));
vec3 n2 = normalize(cross(gl_in[1].gl_Position.xyz - gl_in[2].gl_Position.xyz, gl_in[3].gl_Position.xyz - gl_in[2].gl_Position.xyz));
float edge = dot(n1, viewDirection1) * dot(n2, viewDirection2);
float furLen = texture(furLengthMap, gsinput[1].uv0).r * FUR_LENGTH;
vec4 p[4];
vec3 uv[4];
if (edge > 0 && furLen > 1e-3)
{
p[0] = modelViewProjectionMatrix * vec4(gl_in[1].gl_Position.xyz, 1);
uv[0] = vec3(gsinput[1].uv0, 0);
p[1] = modelViewProjectionMatrix * vec4(gl_in[2].gl_Position.xyz, 1);
uv[1] = vec3(gsinput[2].uv0, 0);
p[2] = modelViewProjectionMatrix * vec4(gl_in[1].gl_Position.xyz + gsinput[1].normal * furLen, 1);
uv[2] = vec3(gsinput[1].uv0, FUR_LAYERS - 1);
p[3] = modelViewProjectionMatrix * vec4(gl_in[2].gl_Position.xyz + gsinput[2].normal * furLen, 1);
uv[3] = vec3(gsinput[2].uv0, FUR_LAYERS - 1);
gl_Position = p[2]; texcoords = uv[2];
EmitVertex();
gl_Position = p[1]; texcoords = uv[1];
EmitVertex();
gl_Position = p[0]; texcoords = uv[0];
EmitVertex();
EndPrimitive();
gl_Position = p[1]; texcoords = uv[1];
EmitVertex();
gl_Position = p[2]; texcoords = uv[2];
EmitVertex();
gl_Position = p[3]; texcoords = uv[3];
EmitVertex();
EndPrimitive();
}
}
#include <common.h.hlsl>
struct PS_INPUT
{
float4 position : SV_POSITION;
float3 uv0 : TEXCOORD0;
float3 tangent : TEXCOORD1;
float3 normal : TEXCOORD2;
float3 worldPos : TEXCOORD3;
};
texture2D diffuseMap : register(t1);
texture3D furMap : register(t2);
SamplerState defaultSampler : register(s0);
float4 main(PS_INPUT input) : SV_TARGET
{
const float specPower = 30.0;
float3 coords = input.uv0 * float3(FUR_SCALE, FUR_SCALE, 1.0f);
float4 fur = furMap.Sample(defaultSampler, coords);
clip(fur.a - 0.01);
float4 outputColor = float4(0, 0, 0, 0);
outputColor.a = fur.a * (1.0 - input.uv0.z);
outputColor.rgb = diffuseMap.Sample(defaultSampler, input.uv0.xy).rgb;
float3 viewDirection = normalize(input.worldPos - viewPosition);
float3x3 ts = float3x3(input.tangent, cross(input.normal, input.tangent), input.normal);
float3 tangentVector = normalize((fur.rgb - 0.5f) * 2.0f);
tangentVector = normalize(mul(tangentVector, ts));
float TdotL = dot(tangentVector, light.direction);
float TdotE = dot(tangentVector, viewDirection);
float sinTL = sqrt(1 - TdotL * TdotL);
float sinTE = sqrt(1 - TdotE * TdotE);
outputColor.xyz = light.ambientColor * outputColor.rgb +
light.diffuseColor * (1.0 - sinTL) * outputColor.rgb +
light.specularColor * pow(abs((TdotL * TdotE + sinTL * sinTE)), specPower) * FUR_SPECULAR_POWER;
float shadow = input.uv0.z * (1.0f - FUR_SELF_SHADOWING) + FUR_SELF_SHADOWING;
outputColor.rgb *= shadow;
return outputColor;
}
#version 430 core
in VS_OUTPUT
{
vec3 uv0;
vec3 normal;
vec3 tangent;
vec3 worldPos;
} psinput;
out vec4 outputColor;
const float FUR_LAYERS = 16.0f;
const float FUR_SELF_SHADOWING = 0.9f;
const float FUR_SCALE = 50.0f;
const float FUR_SPECULAR_POWER = 0.35f;
// lights
struct LightData
{
vec3 position;
uint lightType;
vec3 direction;
float falloff;
vec3 diffuseColor;
float angle;
vec3 ambientColor;
uint dummy;
vec3 specularColor;
uint dummy2;
};
layout(std430) buffer lightsDataBuffer
{
LightData lightsData[];
};
uniform sampler2D diffuseMap;
uniform sampler2DArray furMap;
uniform vec3 viewPosition;
void main()
{
const float specPower = 30.0;
vec3 coords = psinput.uv0 * vec3(FUR_SCALE, FUR_SCALE, 1.0);
vec4 fur = texture(furMap, coords);
if (fur.a < 0.01) discard;
float d = psinput.uv0.z / FUR_LAYERS;
outputColor = vec4(texture(diffuseMap, psinput.uv0.xy).rgb, fur.a * (1.0 - d));
vec3 viewDirection = normalize(psinput.worldPos - viewPosition);
vec3 tangentVector = normalize((fur.rgb - 0.5) * 2.0);
mat3 ts = mat3(psinput.tangent, cross(psinput.normal, psinput.tangent), psinput.normal);
tangentVector = normalize(ts * tangentVector);
float TdotL = dot(tangentVector, lightsData[0].direction);
float TdotE = dot(tangentVector, viewDirection);
float sinTL = sqrt(1 - TdotL * TdotL);
float sinTE = sqrt(1 - TdotE * TdotE);
outputColor.rgb = lightsData[0].ambientColor * outputColor.rgb +
lightsData[0].diffuseColor * (1.0 - sinTL) * outputColor.rgb +
lightsData[0].specularColor * pow(abs((TdotL * TdotE + sinTL * sinTE)), specPower) * FUR_SPECULAR_POWER;
float shadow = d * (1.0 - FUR_SELF_SHADOWING) + FUR_SELF_SHADOWING;
outputColor.rgb *= shadow;
}
В результате мы можем получить таких котиков.
Алгоритм SAF достаточно прост в реализации, однако может существенно усложнить жизнь видеокарте. Каждая модель будет рисоваться несколько раз для получения заданного количества слоёв меха (я использовал 16 слоев). В случае сложной геометрии это может дать существенную просадку производительности. В использованной модели кота покрытая мехом часть занимает примерно 3000 полигонов, следовательно, для рендеринга шкуры будет нарисовано порядка 48000 полигонов. При рисовании «плавников» используется не самый простой геометрический шейдер, что тоже может сказаться в случае высоко детализированной модели.
Замеры производительности велись на компьютере следующей конфигурации: AMD Phenom II X4 970 3.79GHz, 16Gb RAM, AMD Radeon HD 7700 Series, ОС Windows 8.1.
Среднее время кадра. 1920x1080 / MSAA 8x / полный экран
API / Количество котиков | 1 | 25 | 100 |
---|---|---|---|
Direct3D 11 | 2.73615ms | 14.3022ms | 42.8362ms |
OpenGL 4.3 | 2.5748ms | 13.4807ms | 34.2388ms |
Итого, реализация на OpenGL 4 примерно соответствует реализации на Direct3D 11 по производительности на среднем и малом количестве объектов. На большом количестве объектов реализация на OpenGL работает несколько быстрее.
Алгоритм SAF – один из немногих способов реализации меха в интерактивном рендеринге. Однако нельзя сказать, что алгоритм необходим подавляющему числу игр. На сегодняшний день схожий уровень качества (а, возможно, даже более высокий) достигается при помощи арта и умелых рук графического дизайнера. Комбинация полупрозрачных плоскостей с хорошо подобранными текстурами для представления волос и меха – стандарт современных игр, а рассмотренный алгоритм и его вариации, скорее, удел игр будущего.
Автор: rokuz
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/algoritmy/64179
Ссылки в тексте:
[1] статье и демке NVidia: http://developer.download.nvidia.com/SDK/10.5/direct3d/Source/Fur/doc/FurShellsAndFins.pdf
[2] здесь: https://github.com/rokuz/GraphicsDemo
[3] CMake: http://www.cmake.org/
[4] попытки: http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter23.html
[5] здесь: http://steps3d.narod.ru/tutorials/fur-tutorial.html
[6] Блинна-Фонга: http://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_shading_model
[7] здесь: http://steps3d.narod.ru/tutorials/anisotropic-tutorial.html
[8] карта нормалей: http://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%BB%D1%8C%D0%B5%D1%84%D0%BD%D0%BE%D0%B5_%D1%82%D0%B5%D0%BA%D1%81%D1%82%D1%83%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5
[9] Источник: http://habrahabr.ru/post/228753/
Нажмите здесь для печати.