Shells and Finsアルゴリズムを使用したファーレンダリング

画像 こんにちは、Habr! グラフィックスのプログラミングに関する私の今日の投稿は、以前の投稿ほどボリュームがありません。 ほとんどの困難なビジネスでは、時には軽薄なビジネスのための場所があり、今日はシールをレンダリングします。 より正確には、伝統的にDirect3D 11およびOpenGL 4向けのシェルとフィン(SAF)ファーレンダリングアルゴリズムの実装についてお話したいと思います。詳細はcatでお願いします。



SAFファーレンダリングアルゴリズムは、その名前が示すとおり、シェルのレンダリングとフィンのレンダリングの2つの部分で構成されています。 これらの名前は一部の人にとっては面白そうに見えるかもしれませんが、フリースサーフェスの錯覚を作成するアルゴリズムによって作成されたものを完全に反映しています。 Direct3D 10のアルゴリズムの実装の詳細については、 NVidiaの記事とデモを参照してください。Direct3D11とOpenGL 4のデモは、 こちらにあります 。 プロジェクトはDemo_Furと呼ばれます。 ビルドするには、Visual Studio 2012/2013とCMakeが必要です。



シェルとフィンのアルゴリズム



NVidiaにはいくつかの試みがありましたが、毛皮は膨大な数の髪で構成されており、現時点ではそれぞれを個別にリアルタイムで描画することはできません。 フリースの表面の錯覚を作成するために、ボクセルレンダリングを幾分連想させる技術が適用されます。 毛皮の表面の小さな領域である3次元のテクスチャが準備されます。 その中の各ボクセルは、絨毛がそれ自体を通過する確率を決定し、グラフィックの観点から、レンダリング中のある点または別の点での透明度の値を決定します。 このような3次元テクスチャーを生成できます(方法の1つをここで説明します )。 このテクスチャをどのようにレンダリングするかに関して論理的な疑問が生じます。 これを行うために、ジオメトリの周りに「シェル」が描かれます。 このジオメトリを小さな値でスケーリングすることにより形成された、元のジオメトリのコピー。 入れ子人形のようなもので、各レイヤーに3次元のファーテクスチャのレイヤーが重ねられています。 レイヤーは、アルファブレンディングを有効にして順次描画されるため、毛羽立ちのような錯覚が生じます。 ただし、これはマテリアルをファーに似せるには十分ではありません。 目標を達成するには、適切な照明モデルを選択する必要があります。

毛皮は、顕著な異方性材料のカテゴリに属します。 古典的な照明モデル(たとえば、 Blinn-Phongモデル)は、表面を等方性と見なします。 表面特性はその方向とは無関係です。 実際には、これは、プレーンがその法線を中心に回転しても、照明の性質が変わらないことを意味します。 このクラスの照明モデルは、法線と光の入射方向の間の角度を使用してシェーディングを計算します。 異方性照明モデルは、接線(法線に垂直なベクトル、法線および従法線と一緒に基礎を形成する)を使用して照明を計算します。 異方性照明の詳細については、 こちらをご覧ください

異方性ライティングは、ファーレイヤーごとに個別に計算されます。 サーフェス上のあるポイントまたは別のポイントでのタンジェント値は、タンジェントマップを使用して決定されます。 接線マップは、よく知られている法線マップとほぼ同じ方法で形成されます 。 ファーテクスチャの場合、接線ベクトルは正規化された絨毛の方向になります。 したがって、毛皮の3次元テクスチャには4つのチャネルが含まれます。 パックされた接線ベクトルはRGBで保存され、アルファチャネルには絨毛がこのポイントを通過する確率が含まれます。 これにファーのセルフシャドーイングを追加して、かなりリアルに見えるマテリアルを取得します。

人がオブジェクトの外縁を注意深く見ると、錯覚が壊れます。 一方の面が見え、もう一方の面が見えない状況で、面間の特定の角度では、ファーのレイヤーが観察者に見えない場合があります。 この状況を回避するために、追加のジオメトリがそのようなエッジに形成され、法線に沿って延びます。 結果は、魚のヒレにやや似ており、アルゴリズムの名前の2番目の部分につながりました。



Direct3D 11およびOpenGL 4での実装



両方のAPIの実装は一般的に同一であり、わずかな詳細のみが異なります。 次のスキームに従ってレンダリングします。

  1. シーンの毛皮でない部分のレンダリング。 私のデモでは、このような部品に標準のBlinn-Fong照明モデルを使用しています。
  2. フィンのレンダリング。 ジオメトリシェーダーを使用して、ジオメトリストレッチを実装します。 2つのポリゴン間でエッジを拡張する必要があるかどうかを理解するには、このエッジがオブジェクトに対して外部にあるかどうかを判断する必要があります。 このサインは、ポリゴンの法線と正規化されたビジョンベクトル間の角度の値になります。 これらの値の符号が異なる場合、エッジは外部になるため、拡張する必要があります。 Direct3DおよびOpenGLのジオメトリシェーダーは、限られた数のプリミティブで機能します。 1つの共通エッジで2つの隣接するポリゴンを同時に処理する必要があります。 この構造を表すには、4つの頂点が最低限必要です。これは、左の図に明確に示されています。



    図の右側は、共通のリブ1-2の延長と、2つの新しいポリゴン1-5-6および1-6-2の形成を示しています。

    4つの頂点で構成されるプリミティブは、D3D_PRIMITIVE_TOPOLOGY_LINELIST_ADJ(OpenGLではGL_LINES_ADJACENCY)です。 それを使用するには、特別なインデックスバッファを準備する必要があります。 このようなバッファーは、3Dモデル内の三角形の隣接関係にデータがある場合に構築するのに十分簡単です。 インデックスバッファには、2つの隣接する三角形を記述する4つのインデックスのグループが含まれます。

    すべてのモデルが隣接データを簡単に取得できるわけではないことに注意してください。 ほとんどの平滑化モデルでは、これは問題ではありませんが、通常、平滑化グループの境界では、正しいライティングを実現するために頂点が複製されます。 これは、視覚的な隣接が存在する場合、インデックスバッファに隣接が事実上存在しないことを意味します。 この場合、インデックスだけでなく、空間内の頂点の実際の配置によって導かれて、隣接する三角形を検索する必要があります。 この場合、できるだけ多くの三角形を1つの面に分割できるため、このタスクはそれほど簡単ではありません。

    フィンを引くための幾何学的なシェーダーは、スポイラーの下に示されています。



    Direct3D 11用のHLSLジオメトリシェーダー
    #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(); } }
          
          





    OpenGL 4.3用GLSLジオメトリックシェーダー
     #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(); } }
          
          







  3. 甲羅のレンダリング。 明らかに、適切な数のファーのレイヤーを取得するには、ジオメトリを数回描画する必要があります。 ジオメトリを繰り返し描画するには、ハードウェアのインスタンス化を使用します。 シェーダーでファーの特定のレイヤーが描画されるかどうかを判断するには、Direct3DのSV_InstanceIDとOpenGLの変数gl_InstanceIDのセマンティクスを使用するだけで十分です。

    ファーを照らすために、Kajiya-Kay異方性モデルを使用しました。 重要な詳細は、毛皮の長さを設定するための特別なテクスチャの使用でした。 このテクスチャは、予期しない場所(たとえば、猫の目の周り)に長い毛皮が現れるのを防ぐために必要です。 ファーライティングを計算するためのピクセルシェーダーとフラグメントシェーダーを、スポイラーの下に示します。



    Direct3D 11用のHLSLピクセルシェーダー
     #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; }
          
          





    OpenGL 4.3用のGLSLフラグメントシェーダー
     #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; }
          
          





その結果、そのような猫を得ることができます。









比較のために、右の写真は法線マップを使用したBlinn-Fongモデルを使用した猫のレンダリングを示しています。





性能



SAFアルゴリズムの実装は非常に簡単ですが、ビデオカードの寿命を大幅に複雑にする可能性があります。 各モデルは、指定された数のファーレイヤーを取得するために数回描画されます(16レイヤーを使用しました)。 複雑なジオメトリの場合、これによりパフォーマンスが大幅に低下します。 使用済みの猫モデルでは、毛皮で覆われた部分は約3000ポリゴンを占めるため、スキンをレンダリングするために約48000ポリゴンが描画されます。 「フィン」を描画する場合、最も単純な幾何学的シェーダーではなく、非常に詳細なモデルに影響する可能性のあるものが使用されます。

パフォーマンスの測定は、AMD Phenom II X4 970 3.79GHz、16Gb RAM、AMD Radeon HD 7700シリーズ、OS 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アルゴリズムは、インタラクティブレンダリングでファーを実装する数少ない方法の1つです。 ただし、ほとんどのゲームでアルゴリズムが必要であるとは言えません。 これまで、アートの助けとグラフィックデザイナーの熟練した手によって、同様のレベルの品質(および場合によってはさらに高い品質)が達成されています。 半透明のプレーンと髪や毛皮を表現するために適切に選択されたテクスチャの組み合わせは、現代のゲームの標準であり、考慮されるアルゴリズムとそのバリエーションは、むしろ将来のゲームの運命です。



All Articles