NEUROMANTIC

自分でC/C++/UE4/Graphics/ゲームなどをやったことをメモするブログ

「Advanced Terrain Texture Splatting」記事を分析・KodeLifeで実装してみた。

www.gamasutra.com

分析メモ

記事では2つの地形テクスチャマップに対して、3つの方法を提示しています。

  1. 単純にover operatorを使用してマップをブレンディング。(頭悪い)
  2. 各テクスチャのDisplacementかHeightマップの値をお互いに比較して、テクスチャを選択する方法。
  3. 2.から変形したもので、スムースにブレンディングして最終色を決める方法。

1. 単純なブレンディング

  • over operatorとは、以下の数式のことをいう。

c_f = c_sa_s + c_d(1 - a_d)

  • a_sは何でも出来るが、元記事ではa1a2という、地面ごとに割り当てられたオフセット値を使用してブレンディングしている。従って、次のようになる。
    • Unity3Dは地面の各テクスチャ種類のオフセット値を適用して、ブレンディングすることで基本地面をレンダリングしているそうだ。

c_f = c_1a_1 + c_2a_2

  • この地形レンダリング方法は、地形の変化によってスムースに地形の転換が出来るけど、ちょっと不自然に見えがちである。例えば、岩が砂に徐々に変えていく形になるが、現実ではそうではない場合が普通だ。そして、岩の中に砂が入っているのも現実ではよく見られるが、この方法じゃ実装ができない。

もしかして、各テクスチャの明るさ(Grayscale)が以下のようだとしよう。

f:id:neuliliilli:20190521185222p:plain

青いラインは砂の明るさ(高さ)を、そして赤いラインは岩の高さだとして、各ピクセルに赤いラインと青いラインの中で一番高いラインのテクスチャのピクセルが描画されるとする。そうしたら、岩に砂が入って混ざって見えるように描画することが出来るだろう。

元記事では次のようなコードでテクスチャのブレンディングを行っている。

f:id:neuliliilli:20190521190429p:plain

2. Displacementを比較したテクスチャの選別的レンダリング

  • over Operatorみたいな演算をするが、ただ加算するのではなくてオフセット値を掛け算したものを比較してどれかが「高さ」が高いかを比較して一個の値だけを選んで描画する。よって、スムースな地形の変化はなくなったが、より現実的な地形の変化になる。\

f:id:neuliliilli:20190521190617p:plain

  • ただし、テクスチャが混合して描画されるときに、ピクセル単位として地形変化に何のブレンディングをしないため、境界面でアーティファクトが見えたりすることはある。\
    • 対策として、アーティファクトが見えにくくなるテクスチャを選んで描画したり、それとも下(3)の方法を使用する。

f:id:neuliliilli:20190521191312p:plain
境界面にピクセル単位としてアーティファクトが起こるのが見える。

3. 2.に加えてスムースにブレンディング

  • 各テクスチャの高さ(Displacement & Height)が交差する時だけ、ブレンディングを行ってスムースながらも現実的にレンダリングさせて行く。Depthという、交差の範囲を決めて、テクスチャの最終的な高さが範囲に入り、交差しているだけ各高さに対する調整したOffsetを算出して描画する時に使う。

f:id:neuliliilli:20190521192227p:plain
ハイライトしている部分は、`Depth`によるブレンディング範囲。

サンプルのコードは以下のようである。

float3 blend(float4 texture1, float a1, float4 texture2, float a2)
{
    float depth = 0.2;
    float ma = max(texture1.a + a1, texture2.a + a2) - depth;

    float b1 = max(texture1.a + a1 - ma, 0);
    float b2 = max(texture2.a + a2 - ma, 0);

    return (texture1.rgb * b1 + texture2.rgb * b2) / (b1 + b2);
}

f:id:neuliliilli:20190521192325p:plain
blend関数からのカラー値は、平準化によって合計1となる。

  • この方法の問題となれる部分は、テクスチャによってDepthをいちいち調整する必要があること。

KodeLifeで実装

f:id:neuliliilli:20190520210119p:plain
無し+3つのブレンディングを実装して見たもの

元記事でのテクスチャとほぼ同じであるテクスチャを使用して3つの方法と、ブレンディングしてないケースをKodeLifeで実装してみました。 元記事とはちょっと違う点は、元記事ではテクスチャの高さを求めるときに、a_1a_2を使用しますけど、こっちではただ画面の左から右へとのスクリーン上座標範囲[0, 1]をoffsetとして使用します。

Vertex Shader

#version 150

uniform vec2 resolution;
uniform mat4 mvp;

in vec4 a_position;
in vec2 a_texcoord;
out vec2 texCoord;
out vec2 absolutePos;

vec2 GetUniformUvOfScreen(in vec2 texCoord, in vec2 resolution)
{
    return (texCoord * resolution) / min(resolution.x, resolution.y);
}

void main(void)
{
    gl_Position = mvp * a_position;
    texCoord = GetUniformUvOfScreen(a_texcoord, resolution);
    absolutePos = a_texcoord * resolution;
}

Fragment Shader

  • 1番目方法はModeJustBlend関数として実装。
  • 2番目方法はModeBlendCompDisp関数で実装。
  • 3番目方法はModeBlendCompDispSmooth関数で実装。
  • ModeNoneはブレンディングしなかった場合に素のテクスチャを描画するだけの関数。
#version 150
#define DEBUG

uniform vec2 resolution;
uniform sampler2D texture0;
uniform sampler2D texture0_disp;
uniform sampler2D texture1;
uniform sampler2D texture1_disp;

in vec2 texCoord;
in vec2 absolutePos;
out vec4 fragColor;

vec4 GetScaledColorOf(in sampler2D tex, in vec2 uv, in vec2 scale)
{
    return texture(tex, uv / scale);
}

vec3 ModeNone(vec3 tex0, vec3 tex1)
{
    if (absolutePos.x > resolution.x / 2) { return tex1; }
    else { return tex0; }
}

vec3 ModeJustBlend(vec3 tex0, vec3 tex1, float offset)
{
    return mix(tex0, tex1, offset);
}

vec3 ModeBlendCompDisp(
    in vec3 tex0, in float tex0_disp, 
    in vec3 tex1, in float tex1_disp, 
    in float offset)
{
    float disp0 = tex0_disp * (1 - offset);
    float disp1 = tex1_disp * offset;
    
    return disp0 > disp1 ? tex0 : tex1;
}

vec3 ModeBlendCompDispSmooth(vec3 tex0, float disp0, vec3 tex1, float disp1, float offset)
{
    const float depthOffset = 0.1f;
    float d0 = disp0 * (1 - offset);
    float d1 = disp1 * offset;
    float dm = max(d0, d1) - depthOffset;
    
    float b1 = max(d0 - dm, 0);
    float b2 = max(d1 - dm, 0);
    
    return (tex0 * b1 + tex1 * b2) / (b1 + b2);
    
}

void main(void)
{
    const vec2 scale0 = vec2(1.0);
    const vec2 scale1 = vec2(.4, .5);
    
    vec3 tex0 = pow(GetScaledColorOf(texture0, texCoord, scale0).xyz, vec3(1.5));
    vec3 tex1 = pow(GetScaledColorOf(texture1, texCoord, scale1).xyz, vec3(1.2));
    float dep0 = GetScaledColorOf(texture0_disp, texCoord, scale0).x;
    float dep1 = GetScaledColorOf(texture1_disp, texCoord, scale1).x;
    vec2 offset = absolutePos / resolution;
    
    if (offset.y >= 0.75f)
    {   // NONE
        fragColor = vec4(ModeNone(tex0, tex1), 1.);
    }
    else if (offset.y >= 0.5f)
    {   // JUST BLEND (OVER OPERATOR)
        fragColor = vec4(ModeJustBlend(tex0, tex1, offset.x), 1.);
    }
    else if (offset.y >= 0.25f)
    {   // MODE_BLEND_COMP_DISP
        fragColor = vec4(ModeBlendCompDisp(tex0, dep0, tex1, dep1, offset.x), 1.);
    }
    else
    {   // MODE_BLEND_COMP_DISP_SMOOTH
        fragColor = vec4(ModeBlendCompDispSmooth(tex0, dep0, tex1, dep1, offset.x), 1.);
    }
}

参照

  • リソース提供

cc0textures.com