NEUROMANTIC

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

「Heartfelt」を分析・KodeLifeで応用してみた。

www.shadertoy.com

分析しにくかったシェーダー効果でしたけど、なんとか分析して変数をuniformで作成してKodeLifeで実装してみました。(なお、不必要なコードも削除)

  • CHEAP_NORMALSは除きます。コメントでは2倍見にくくなるよと書いていたんですが、実際にやってみると3倍以上見にくくなります。もしかして水滴のぼかしを追加で行いたいならやっても良いかもしれませんね。
  • ハート模様の雨レンダリングの分析はしませんでした。
  • USE_POST_PROCESSINGの分析はします。
  • 元となるシェーダーは、「表示 - 非営利 - 継承 3.0 非移植」のライセンスによって営利的に利用してはなりません。ですから上のようなシェーダー効果を商用ゲームなどで出せるには一から作り直す必要があります。
  • 率直に言うと、アルゴリズム自体はものすごく感動しましたけど、素のままじゃコードが見にくいです。
続きを読む

「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

「Cathode」を参照・分析・応用してみた。

ブラウン管みたいにスキャンライン、そして色の滲みを表現するポストプロセッシングシェーダーを分析、応用してみました。

分析

MainImage関数

MainImageは各描画セルの腐食によるスキャンラインを含むもの、ぼかしだけをやって描画するもの、そして単にピクセルを拡大して描画するようとしてます。 それぞれの部分の境界線はsmoothstepを使用して枠線を引いてますね。

    vec3 col = phosphors(fragCoord.xy/4.0, iChannel0);
    if (f.y > 200.)
        col = phosphors(fragCoord.xy/4.0 - vec2(0,30.), iChannel1);
  • phosphors名は「蛍光体」というもので、この関数は蛍光体を使用したブラウン管の画面描画や蛍光体による腐食現象を再現して描画する。
  • iChannel0iChannel1は各テクスチャバッファを示す。なので下の部分と上の部分で違ったテクスチャを描画している。
  • fragCoord.xyは画面のピクセル単位の座標値を示す。
    • もし推測が正しければ、fragCoordgl_TexCoordと同じで、(x + 0.5, y + 0.5)の座標値を持つかもしれない。
  • 4.0ズーム値を示す。

phosphors関数

vec3 col = vec3(0);
p -= 0.25;
p.y += mod(gl_FragCoord.x,2.)<1.?.03:-0.03;
p.y += mod(gl_FragCoord.x,4.)<2.?.02:-0.02;
  • colは蛍光体関数ロジックの最後に決まった色である。
  • p -= 0.25は、蛍光体の位置を調整する。またp.yの位置を調整するのは、各燐光体がずらりを並ぶことになるとパターンが認識しやすいので、それを防ぐために微細に調整したものかもしれない。
for(int i=-2;i<=2;i++)
for(int j=-2;j<=2;j++)
{
    vec2 tap = floor(p) + 0.5 + vec2(i,j);
    vec3 rez = texture(tex, tap/iChannelResolution[0].xy).rgb; //nearest neighbor
  • 今位置する蛍光体からある範囲の隣接した蛍光体の色を使用して最終の色を決める。
  • floor(p) + 0.5をする理由は(確かではないが)Nearestな色を取るためには各バッファのピクセルの中間地点が必要となるのに、pの定数部分だけが必要となるからだと思われる。
  • iChannelResolution[0]は各テクスチャバッファのサイズを示す。textureはUVとして[0, 1]の座標を求む。
//center points
float rd = sqd(tap, p + vec2(0.0,0.2));//distance to red dot
const float xoff = .25;
float gd = sqd(tap, p + vec2(xoff,.0));//distance to green dot
float bd = sqd(tap, p + vec2(-xoff,.0));//distance to blue dot
  • 蛍光体の各セル「G、R、B」の各距離値を得るためにsqdという関数を呼び出す。
    • 一般的には「R、G、B」が普通だけど、なぜかこのシェーダーでは「G」が中央になっている。
  • spd関数は以下のように最適化か余計なものを省けられる。
  • `xoff``は、各チャンネルの蛍光体がお互いに離れている程度を示す。
    • 個人的には0.25から0.5のほうが良かった。
    • もし、古すぎて色がちゃんと出せない時の効果をやらせるなら、xoffを2から3にすればいいと思う。
// Phosphor shape
float Sqd(in vec2 a, in vec2 b)
{
    // Range of [x-scanline, y-scanline]
    a = (a - b) * vec2(1.0f, 2.0f);
    return mix(max(abs(a.x), abs(a.y)), length(a), 0.3f);
}
  • spd関数のvec2の値は、横または縦のスキャンラインを作るのに必要となる。
    • 値が大きくなると、腐食が深化してスキャンラインの許容範囲が大きくなる。
rez = pow(rez,vec3(1.18)) * 1.08;
rez.r *= decay(rd);
rez.g *= decay(gd);
rez.b *= decay(bd);
  • 求めたrez(今のイテレーションで得られた色)の各チャンネルをdecay関数を呼び出しした結果で侵食させる。
  • powをする理由は、多分カラースペースを非線形に合わせるために調整したものだと見られる。(GammaのDecode過程でも見られそう)
    • 1.08を外して1.182.2にしたら色が暗くなるが、濃くみえるようになる。
//Phosphor decay
float decay(in float d)
{
    return mix(exp2(-d*d*2.5-.3),0.05/(d*d*d*0.45+0.055),.65)*0.99;
}
  • わけわからない(MagicNumberである可能性が非常に高い)数式を使用して腐食値を決める。

応用(KodeLifeを使用)

youtu.be

前日実装した「ゆらゆら」に「Cathode」効果を適用させてみました。zoomで、画面描画のズームインを出来るようにし、各蛍光体には一つの色しか出せないよう、UVを画面用とゆらゆらの描画用を一つずつ用意して各蛍光体に一つの色がまとまってレンダリングさせるようにしました。

今のコードではレンダリングパスが一つなのでちょっと複雑になりましたが、「ゆらゆら」の描画部分をテクスチャにまとめて、次のパスで「Cathode」効果を実装すればもっと良いのではと思います。

Vertex Shader

#version 150

uniform vec2 resolution;
uniform mat4 mvp;
uniform int zoom = 8;

in vec4 a_position;
in vec3 a_normal;
in vec2 a_texcoord;

out VertexData
{
    vec2 v_texcoord;
    vec2 v_zoomCoord;
} outData;
varying float v_cellOffset;

void main(void)
{
    // Some drivers don't like position being written here
    // with the tessellation stages enabled also.
    // Comment next line when Tess.Eval shader is enabled.
    gl_Position = mvp * a_position;

    outData.v_texcoord = (a_texcoord * resolution) / min(resolution.x, resolution.y);
    outData.v_zoomCoord = (a_texcoord * resolution) / zoom;
    v_cellOffset = min(resolution.x, resolution.y) / zoom;
}

Fragment Shader

#version 150

uniform float time;

in VertexData
{
    vec2 v_texcoord;
    vec2 v_zoomCoord;
} inData;
varying float v_cellOffset;

out vec4 fragColor;

const vec3 col1 = vec3(1, 0, 0.25); // off1 color (primary)
const vec3 col2 = vec3(0, 0.3f, 1); // off2 color (secondary)
const vec3 col3 = vec3(1, 1, 1);    // off3 color (sub)

vec3 Way3Color(in vec2 uv)
{ 
    float speed = 0.125f;
    vec2 p = uv * 5.0f;
    for (int i = 1; i < 10; ++i)
    {
        float fi = float(i);
        p.x += 0.4f / fi * sin(fi * 3.0f * p.y + time  * speed) + 0.005f;
        p.y += 0.4f / fi * sin(fi * 3.0f * p.x + time  * speed) + 0.005f;
    }
    
    // [0, 1]
    float off1 = cos(p.x + p.y) * 0.5f + 0.5f;
    float off2 = sin(p.x + p.y) * 0.5f + 0.5f;
    float off3 = (2.0f - (off1 + off2)) * 0.5f;
       
    // over operator
    vec3 color = vec3(0);
    color = col1 * off1 + color * (1.0f - off1);
    color = col2 * off2 + color * (1.0f - off2);
    color = col3 * off3 + color * (1.0f - off3);
    return color;
}

// Phosphor decay
float Decay(in float d)
{
    return mix(
        exp2(-d * d * 2.5f - 0.3f)
      , 0.05f / (pow(d, 3.0f) * 0.45f + 0.055f)
      , 0.65f);
}

// Phosphor shape (Magic number?)
float Sqd(in vec2 a, in vec2 b)
{
    a -= b;
    // Range of [x-scanline, y-scanline]
    a *= vec2(1.0f, 2.0f); 
    return mix(max(abs(a.x), abs(a.y)), length(a), 0.3f);
}

vec3 Phosphors(in vec2 zoomUv)
{   
    vec3 col = vec3(0);
    // Start phosphors cell from top to avoid patterning.
    zoomUv.y += mod(gl_FragCoord.x, 2.0f) < 1.0f ? 0.03f: -0.03f;
    zoomUv.y += mod(gl_FragCoord.x, 4.0f) < 2.0f ? 0.02f : -0.02f;
    
    //5x5 kernel (this means a given fragment can be affected by a pixel 4 game pixels away)
    const int range = 2;
    for(int i = -range; i <= range; i++)
    {
        for(int j = -range; j <= range; j++)
        {
            // Nearest neighbor
            vec3 rez = Way3Color(inData.v_texcoord - mod(inData.v_texcoord, 1 / v_cellOffset));
            // Center points (5) [R,G,B] in each phosphor cell.
            const float xoff = 0.25f;
            
            // relocate gl_TexCoord (center pixel coordinate)
            vec2 tap = floor(zoomUv) + 0.5f + vec2(i,j); 
            rez = pow(rez,vec3(1.18)) * 1.08; // Decode?
            rez.r *= Decay(Sqd(tap, zoomUv + vec2(xoff, 0.2))); // Distance to red filter
            rez.g *= Decay(Sqd(tap, zoomUv + vec2(0.0, 0.0)));  // Distance to green filter
            rez.b *= Decay(Sqd(tap, zoomUv + vec2(-xoff, 0.0)));// Distance to blue filter

            col += rez;
        }
    }
    
    return col;
}

void main() { fragColor = vec4(Phosphors(inData.v_zoomCoord), 1.0f); }

参照

en.wikipedia.org

ゆらゆら・ShaderToyのテンプレートコードのメモ

ゆらゆら

www.shadertoy.com

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/min(iResolution.x, iResolution.y);

    float speed = 0.5f;
    vec2 p = uv * 5.0f;
    for (int i = 1; i < 10; ++i)
    {
        float fi = float(i);
        vec2 mouse = iMouse.xy / 1000.0f;
        p.x += 0.2f / fi * sin(fi * 3.0f * p.y + iTime * speed) + mouse.x;
        p.y += 0.2f / fi * sin(fi * 3.0f * p.x + iTime * speed) + mouse.y;
    }
    
    vec3 col1 = vec3(1, 0, 0.25); // off1 color (primary)
    vec3 col2 = vec3(0, 0.3f, 1); // off2 color (secondary)
    vec3 col3 = vec3(1, 1, 1);    // off3 color (sub)
    
    // [0, 1]
    float off1 = cos(p.x + p.y) * 0.5f + 0.5f;
    float off2 = sin(p.x + p.y) * 0.5f + 0.5f;
    float off3 = (2.0f - (off1 + off2)) * 0.5f;
       
    // over operator
    vec3 color = vec3(0);
    color = col1 * off1 + color * (1.0f - off1);
    color = col2 * off2 + color * (1.0f - off2);
    color = col3 * off3 + color * (1.0f - off3);
    
    fragColor = vec4(color, 1.0f);
}

久しぶりに触りますので簡単なものを作ってみました。(参照の記事のコードを見てちょっと改造などなど)

テンプレートコード

忘れてた時に見たら良さそうで貼りますね。

// Shader Inputs, uniforms
uniform vec3      iResolution;           // viewport resolution (in pixels)
uniform float     iTime;                 // shader playback time (in seconds)
uniform float     iTimeDelta;            // render time (in seconds)
uniform int       iFrame;                // shader playback frame
uniform float     iChannelTime[4];       // channel playback time (in seconds)
uniform vec3      iChannelResolution[4]; // channel resolution (in pixels)
uniform vec4      iMouse;                // mouse pixel coords. xy: current (if MLB down), zw: click
uniform samplerXX iChannel0..3;          // input channel. XX = 2D/Cube
uniform vec4      iDate;                 // (year, month, day, time in seconds)

// Main Funtion
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;
    // [0, 1] Squared UV.
    vec2 uv_min = fragCoord / min(iResolution.x, iResolution.y);
    // Time varying pixel color
    vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
    // Output to screen
    fragColor = vec4(col,1.0);
}

参照

viclw17.github.io

「Blistering Paint」シェーダーをShadeで真似してみた。

内容

成功例が出せてから後ほど追加します。🙏

Shade実装動画(失敗例)

youtu.be

参照

GDC 2017: Substance pipeline and toolset for Paragon heroes w/ Brad Smith - YouTube

https://www.artstation.com/artwork/xzDLvX