ShaderGraphでVoronoiエッジ距離

環境

Unity2022.2.2f1

ShaderGraph14.0.4

概要

CustomFunctionノードを使用して、標準のvoronoiノードにプラスしてエッジまでの距離を返すようにしたノード用の関数を作成してみました。

※for文を[unroll]しないと自分の環境だとバグるので入れています。

コード

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Hashes.hlsl"

float2 Unity_Voronoi_RandomVector_Deterministic_float (float2 UV, float offset)
{
    Hash_Tchou_2_2_float(UV, UV);
    return float2(sin(UV.y * offset), cos(UV.x * offset)) * 0.5 + 0.5;
}

void VoronoiWithDistance_float(float2 UV, float AngleOffset, float CellDensity, out float Out, out float Cells, out float Distance)
{
    // first pass: regular voronoi
    float2 g = floor(UV * CellDensity);
    float2 f = frac(UV * CellDensity);
    float t = 8.0;
    float3 res = float3(8.0, 0.0, 0.0);

    float2 mr;

    [unroll]
    for (int y = -1; y <= 1; y++)
    {
        [unroll]
        for (int x = -1; x <= 1; x++)
        {
            float2 lattice = float2(x, y);
            float2 offset = Unity_Voronoi_RandomVector_Deterministic_float(lattice + g, AngleOffset);
            float d = distance(lattice + offset, f);
            if (d < res.x)
            {
                res = float3(d, offset.x, offset.y);
                Out = res.x;
                Cells = res.y;

                mr = lattice + offset - f;
            }
        }
    }
    // second pass: distance to borders
    float md = 8.0;
    [unroll]
    for (int y = -2; y <= 2; y++)
    {
        [unroll]
        for (int x = -2; x <= 2; x++)
        {
            float2 lattice = float2(x, y);
            float2 offset = Unity_Voronoi_RandomVector_Deterministic_float(lattice + g, AngleOffset);
            float2 r = lattice + offset - f;

            if (dot(mr - r, mr - r) > 0.0001)
            {
                md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
            }
        }
    }
    Distance = md;//float3(md, mr.x, mr.y);
}

void VoronoiGetBorder_float(float Distance, float Length, out float Out)
{
    Out = 1.0 - smoothstep(0.0, Length, Distance);
}

参考

Inigo Quilez :: computer graphics, mathematics, shaders, fractals, demoscene and more

【Unity】リピート感なしのテクスチャマッピングの再現【Blender】 - 夜はCGの時間

Unity Shader Graph custom node for precise voronoi borders · GitHub

[Unity] Shader Graph でノイズ関数を改造してタイリングに対応するカスタムノードを作る(改) - Qiita

わけわからない

標準のVoronoiノードが正しく動作しない。赤くなる。何でこんな結果になるのか。

コードでも赤になる。

ただしVoronoi関数のforの上に[unroll]を付けると直る。

正しいのかはわからない。

Shader "Unlit/TestVoronoi"
{
    Properties
    {
    }

    CGINCLUDE
    //#pragma enable_d3d11_debug_symbols
    #include "UnityCG.cginc"
    // Graph Includes
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Hashes.hlsl"
        
    // Graph Functions
    float2 Unity_Voronoi_RandomVector_Deterministic_float (float2 UV, float offset)
    {
        Hash_Tchou_2_2_float(UV, UV);
        return float2(sin(UV.y * offset), cos(UV.x * offset)) * 0.5 + 0.5;
    }
        
    void Unity_Voronoi_Deterministic_float(float2 UV, float AngleOffset, float CellDensity, out float Out, out float Cells)
    {
        float2 g = floor(UV * CellDensity);
        float2 f = frac(UV * CellDensity);
        float t = 8.0;
        float3 res = float3(8.0, 0.0, 0.0);
        [unroll]
        for (int y = -1; y <= 1; y++)
        {
            [unroll]
            for (int x = -1; x <= 1; x++)
            {
                float2 lattice = float2(x, y);
                float2 offset = Unity_Voronoi_RandomVector_Deterministic_float(lattice + g, AngleOffset);
                float d = distance(lattice + offset, f);
                if (d < res.x)
                {
                    res = float3(d, offset.x, offset.y);
                    Out = res.x;
                    Cells = res.y;
                }
            }
        }
    }
        
    void Unity_ColorspaceConversion_HSV_RGB_float(float3 In, out float3 Out)
    {
        float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
        float3 P = abs(frac(In.xxx + K.xyz) * 6.0 - K.www);
        Out = In.z * lerp(K.xxx, saturate(P - K.xxx), In.y);
    }

    ENDCG

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float voroValue;
                float voroCells;
                Unity_Voronoi_Deterministic_float(i.uv, 6, 5, voroValue, voroCells);
                fixed3 rgb;
                Unity_ColorspaceConversion_HSV_RGB_float(float3(voroCells, 1, 1), rgb);
                return fixed4(rgb, 1);
            }
            ENDCG
        }
    }
}

描画順に依存しない半透明処理(OIT)

環境

Unity2022.2.2f1

概要

画像は複数のオブジェクトがあるように見えますが1つです。

Unityで生成した複数のオブジェクトのMeshをCombineMeshして1つのMeshにしています。

Unityだと通常、半透明はオブジェクト単位でソートされ、画面奥からZ書き込みなしで描画します。

半透明のMesh自体が複雑だと、オブジェクト単位のソートでは見え方が破綻してしまいます。

このOITの手法はピクセル単位でわりと正しく見えるように近似するものです。

詳しくは参考のページに丁寧に書いてあります。

コード

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UI;

public class TestOit : MonoBehaviour
{
    [SerializeField] Shader _shClear = null;
    [SerializeField] Shader _shCombine = null;
    private RenderTexture[] _renderTexs = new RenderTexture[2];
    private Material _matClear = null;
    private Material _matCombine = null;
    private static int _spTargetTex0 = Shader.PropertyToID("_TargetTex0");
    private static int _spTargetTex1 = Shader.PropertyToID("_TargetTex1");

    private void OnEnable()
    {
        _matClear = new Material(_shClear);
        _matCombine = new Material(_shCombine);

        var camera = Camera.main;
        _renderTexs[0] = new RenderTexture(camera.pixelWidth, camera.pixelHeight, 24, UnityEngine.Experimental.Rendering.DefaultFormat.HDR);
        _renderTexs[1] = new RenderTexture(camera.pixelWidth, camera.pixelHeight, 0, UnityEngine.Experimental.Rendering.DefaultFormat.HDR);

        var rts = new RenderBuffer[]
        {
            _renderTexs[0].colorBuffer,
            _renderTexs[1].colorBuffer,
        };
        camera.SetTargetBuffers(rts, _renderTexs[0].depthBuffer);
        Shader.SetGlobalTexture(_spTargetTex0, _renderTexs[0]);
        Shader.SetGlobalTexture(_spTargetTex1, _renderTexs[1]);

        var cbClear = new CommandBuffer();
        cbClear.name = "OitClear";
        cbClear.DrawProcedural(Matrix4x4.identity, _matClear, 0, MeshTopology.Quads, 4, 1);
        camera.AddCommandBuffer(CameraEvent.AfterSkybox, cbClear);

        var cbCombine = new CommandBuffer();
        cbCombine.name = "OitCombine";
        cbCombine.SetRenderTarget(-1);
        cbCombine.Blit(null, BuiltinRenderTextureType.CurrentActive, _matCombine);
        camera.AddCommandBuffer(CameraEvent.AfterEverything, cbCombine);
    }

    private void OnDisable()
    {
        Camera.main.targetTexture = null;
        for(int i = 0; i < _renderTexs.Length; i++)
            _renderTexs[i].Release();

        if(_matClear != null)
            GameObject.Destroy(_matClear);
        if(_matCombine != null)
            GameObject.Destroy(_matCombine);
    }
}
Shader "Hidden/OIT/Clear"
{
    Properties
    {
    }
    SubShader
    {
        ZTest Always
        Cull Back
        ZWrite Off
        Fog { Mode Off }
        // Color0
        ColorMask A 0
        // Color1

        Pass
        {
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            struct a2v
            {
                uint vertexId : SV_VertexID;
            };
            
            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            struct outf
            {
                fixed4 color0 : SV_Target0;
                fixed4 color1 : SV_Target1;
            };
            
            v2f vert(a2v v)
            {
                float3 posCSs[4] = 
                {
                    float3(-1,  1, 0),
                    float3(-1, -1, 0),
                    float3( 1, -1, 0),
                    float3( 1,  1, 0),
                };
                v2f o;
                o.pos = float4(posCSs[v.vertexId], 1);
                return o;
            }
            
            outf frag(v2f i)
            {
                outf o;
                o.color0 = fixed4(0,0,0,1);
                o.color1 = fixed4(0,0,0,0);
                return o;
            }
            ENDCG
        }
    }
}
Shader "Hidden/OIT/Combine"
{
    Properties
    {
    }
    SubShader
    {
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off }

        Pass
        {
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            struct a2v
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 texcoord : TEXCOORD0;
            };
            
            sampler2D _TargetTex0;
            sampler2D _TargetTex1;
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.texcoord = v.texcoord;
                return o;
            }
            
            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 background = tex2D(_TargetTex0, i.texcoord);
                float revealage = background.a;
                float4 accum = tex2D(_TargetTex1, i.texcoord);
                fixed3 rgb = fixed3(accum.rgb / clamp(accum.a, 1e-4, 5e4));
                rgb = rgb * (1.0 - revealage) + background.rgb * revealage;
                return fixed4(rgb, 1.0);

            }
            ENDCG
        }
    }
}

「Unlit/OIT/Transparent」このシェーダを半透明オブジェクトに適用する

Shader "Unlit/OIT/Transparent"
{
    Properties
    {
        _Color("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
        LOD 100
        ZWrite Off
        // Color0
        ColorMask A 0
        Blend 0 Zero OneMinusSrcAlpha
        // Color1
        Blend 1 One One

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };
            float4 _Color;

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float posVS_z : TEXCOORD0;
            };

            struct outf
            {
                fixed4 color0 : SV_Target0;
                fixed4 color1 : SV_Target1;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.posVS_z = abs(mul(UNITY_MATRIX_MV, v.vertex).z);
                return o;
            }

            outf frag (v2f i)
            {
                outf o;
                float4 col = _Color;
                //return col;

                o.color0 = fixed4(0,0,0,col.a);
                float z = i.posVS_z;
                float w = col.a * max(1e-2, min(3 * 1e3, 10.0/(1e-5 + pow(z/5, 2) + pow(z/200, 6))));
                o.color1 = fixed4(col.rgb * col.a, col.a) * w;
                return o;
            }
            ENDCG
        }
    }
}

参考

UnityでOIT(Order Independent Tranceparency) - UnityShader 入門

GitHub - happy-turtle/oit-unity: Order-independent Transparency Implementation in Unity with Per-Pixel Linked Lists

GitHub - candycat1992/OIT_Lab: :pencil2: Order-independent Transparent in Unity

もんしょの巣穴 - DirectXの話 第110回

フェイクスポットライト

環境

Unity2022.2.2f1

概要

フェイクスポットライトの実装テストです。

法線を考慮していません。

デカール処理がOFFの場合ならもっとシンプルになりますが、面倒なので分けていません。

円錐の頂点部分が原点、底がZ方向で円の中心部分が0.0,0.0,1.0、半径が0.5のメッシュを使用しています。

コード

Shader "Custom/SSSpotDecalLight"
{
    Properties{
        [HDR]_Color("Color", Color) = (1, 1, 1, 1)
        [KeywordEnum(Off, On)] _VolumeEnabled("Volume Enabled", Float) = 1
        [KeywordEnum(Off, On)] _DecalEnabled("Decal Enabled", Float) = 1
        _MainTex ("MainTex", 2D) = "white" {}
        _Rim ("Rim", Range(0.0, 1.0)) = 1.0
    }

    CGINCLUDE
    #pragma enable_d3d11_debug_symbols
    #pragma shader_feature _ _VOLUMEENABLED_ON
    #pragma shader_feature _ _DECALENABLED_ON
    #include "UnityCG.cginc"

    float4 _Color;
    // Volume
    sampler2D _MainTex;
    float4 _MainTex_ST;
    fixed _Rim;
    // Decal
    sampler2D _CameraDepthNormalsTexture;
    sampler2D _CameraDepthTexture;

    struct appdata
    {
        float4 vertex : POSITION;
        float3 normal : NORMAL;
        float3 texcoord : TEXCOORD0;
    };

    struct v2f
    {
        float4 positionCS :SV_POSITION;
        // Volume
        float4 color : COLOR;
        float2 texcoord : TEXCOORD0;
        // Decal
        float4 positionSS : TEXCOORD1;
        float3 positionWS : TEXCOORD2;
        float dotRadius : TEXCOORD3;
        float3 lightDirVS : TEXCOORD4;
    };
    ENDCG

    SubShader
    {
        Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
        Pass
        {
            ZWrite Off
            Cull Off
            Lighting Off
            Blend SrcAlpha One

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            v2f vert(appdata v)
            {
                v2f o;
                o.positionCS = UnityObjectToClipPos(v.vertex);

                // Decal
                o.positionSS = ComputeScreenPos(o.positionCS);
                o.positionWS = mul(unity_ObjectToWorld, v.vertex).xyz;

                float3 vec = UNITY_MATRIX_M._m02_m12_m22 + UNITY_MATRIX_M._m00_m10_m20 * 0.5;
                o.dotRadius = dot(normalize(vec), normalize(UNITY_MATRIX_M._m02_m12_m22));
                o.lightDirVS = mul(UNITY_MATRIX_V, UNITY_MATRIX_M._m02_m12_m22);

                // Volume
                float3 viewDirOS = normalize(ObjSpaceViewDir(v.vertex));
                float NdotE = abs(dot(v.normal, viewDirOS));
                o.color.rgb = smoothstep(1 - _Rim, 1.0, NdotE) *_Color.rgb;
                o.color.a = 1.0 - (saturate(v.vertex.z - 0.5) / 0.5);

                o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);

                return o;
            }

            half4 frag(v2f i) : Color
            {
                half4 col = fixed4(0,0,0,_Color.a);
                // Decal
                const float far = _ProjectionParams.z;
                float2 positionSS = i.positionSS.xy / i.positionSS.w;
                float depth;
                float3 normalVS;
                float4 depthAndNormal = tex2D(_CameraDepthNormalsTexture, positionSS);
                DecodeDepthNormal(depthAndNormal, depth, normalVS);
                float eyeDepth = depth * far;
                eyeDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, positionSS.xy));
                float3 cameraForwardDir = -UNITY_MATRIX_V._m20_m21_m22;
                float3 viewDirWS = normalize(UnityWorldSpaceViewDir(i.positionWS));
                float3 positionWS = ((eyeDepth * viewDirWS * (1.0 / dot(cameraForwardDir, viewDirWS))) + _WorldSpaceCameraPos);

                float3 vecWS;
                float power;
                float lightRadius = length(UNITY_MATRIX_M._m00_m10_m20) * 0.5;
                half3 rgb = tex2D(_MainTex, i.texcoord);
#if defined(_VOLUMEENABLED_ON)
                // Volume
                vecWS = positionWS - i.positionWS;
                power = saturate(length(vecWS) / lightRadius);
                half3 volumeRgb = rgb * i.color.rgb * i.color.a * power;
                col.rgb += volumeRgb.rgb;
#endif

#if defined(_DECALENABLED_ON)
                float lenLightDirWS = length(UNITY_MATRIX_M._m02_m12_m22);
                vecWS = positionWS - UNITY_MATRIX_M._m03_m13_m23;
                float lenVecWS = length(vecWS);
                power = lenVecWS > lenLightDirWS ? 0 : saturate(1.0 - (lenVecWS - (lenLightDirWS - lightRadius)) / lightRadius);
                float d = dot(normalize(vecWS), normalize(UNITY_MATRIX_M._m02_m12_m22));
                power *= saturate(d - i.dotRadius) / (1.0 - i.dotRadius);

                float NdotL = saturate(dot(normalVS, normalize(-i.lightDirVS)));
                power *= NdotL;

                half3 decalRgb = smoothstep(1 - _Rim, 1.0, power) * power *_Color.rgb;
                col.rgb += decalRgb.rgb;
#endif
                return col;
            }
            ENDCG
        }
    }
}

参考

フェイクポイントライト - テキトープログラム( ..)φメモ

SmokeLighting(SixWayLightmap)

環境

Unity2022.2.2f1

概要

UnityがHDRPでVfxGraphに実装した「Six Way Smoke Lit」的なもののテストです

ポイントライトはRenderModeを「Important」に設定しています

QuadのGameObjectを作成して、マテリアルを設定すれば確認できます

テクスチャは参考のUnityのブログ記事から使えるもののリンクがあります

コード

Shader "lit/SmokeSixWayLightmap"
{
    Properties
    {
        _PisitiveTex("PisitiveTex", 2D) = "white" {}
        _NegativeTex("NegativeTex", 2D) = "white" {}
        _TexDivideX("TexDivideX", int) = 1
        _TexDivideY("TexDivideY", int) = 1
        [HDR]_EmissiveColor("EmissiveColor", Color) = (0,1,0,1)
    }

    CGINCLUDE
    #pragma enable_d3d11_debug_symbols
    #include "UnityCG.cginc"
    #include "Lighting.cginc"

#define BILLBOARD

    struct appdata
    {
        float4 vertex : POSITION;
        float2 texcoord : TEXCOORD;
#if !defined(BILLBOARD)
        float3 normal : NORMAL;
        float4 tangent : TANGENT;
#endif
    };

    struct v2f
    {
        float4 pos : SV_POSITION;
        float2 texcoord : TEXCOORD0;
        float3 lightDirTS : TEXCOORD1;
        float3 lightDirTS2 : TEXCOORD2;
        float3 lightColor : TEXCOORD3;
        float3 lightColor2 : TEXCOORD4;
    };

    fixed4 _EmissiveColor;
    sampler2D _PisitiveTex;
    sampler2D _NegativeTex;
    uint _TexDivideX;
    uint _TexDivideY;

    float ComputeLightMap(float3 lightDirTS, float4 colPositiveRTBA, float4 colNegativeLBFE)
    {
        float lr = (lightDirTS.x > 0.0) ? (colPositiveRTBA.x) : (colNegativeLBFE.x);
        float tb = (lightDirTS.y > 0.0) ? (colPositiveRTBA.y) : (colNegativeLBFE.y);
        float fb = (-lightDirTS.z > 0.0) ? (colPositiveRTBA.z) : (colNegativeLBFE.z);
        float lightMap = lr * lightDirTS.x * lightDirTS.x
                       + tb * lightDirTS.y * lightDirTS.y
                       + fb * lightDirTS.z * lightDirTS.z;
        return lightMap;
    }

    // Shade4PointLightsから法線の処理を消したもの
    float3 FourPointLightsColor(
        float4 lightPosX, 
        float4 lightPosY, 
        float4 lightPosZ,
        float3 lightColor0, 
        float3 lightColor1, 
        float3 lightColor2, 
        float3 lightColor3,
        float4 lightAttenSq,
        float3 pos)
    {
        // to light vectors
        float4 toLightX = lightPosX - pos.x;
        float4 toLightY = lightPosY - pos.y;
        float4 toLightZ = lightPosZ - pos.z;

        // squared lengths
        float4 lengthSq = 0;
        lengthSq += toLightX * toLightX;
        lengthSq += toLightY * toLightY;
        lengthSq += toLightZ * toLightZ;

        // don't produce NaNs if some vertex position overlaps with the light
        lengthSq = max(lengthSq, 0.000001);

        // attenuation
        float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
        float4 diff = atten;

        // final color
        float3 col = 0;
        col += lightColor0 * diff.x;
        col += lightColor1 * diff.y;
        col += lightColor2 * diff.z;
        col += lightColor3 * diff.w;

        return col;
    }

    ENDCG

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "RenderType"="Transparent"
            "IgnoreProjector"="True"
            "LightMode" = "ForwardBase"
        }
        Pass
        {
            ZWrite Off
            Cull Back
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap
            #pragma vertex vert
            #pragma fragment frag

            v2f vert(appdata v)
            {
                v2f o;
#if defined(BILLBOARD)
                // Billboard
                float3x3 mtxRot = unity_ObjectToWorld;
                mtxRot._m00_m10_m20 = normalize(mtxRot._m00_m10_m20);
                mtxRot._m01_m11_m21 = normalize(mtxRot._m01_m11_m21);
                mtxRot._m02_m12_m22 = normalize(mtxRot._m02_m12_m22);
                v.vertex.xyz = mul(v.vertex.xyz, mul(UNITY_MATRIX_V, mtxRot));
#endif
                // TextureAnimation
                const uint dx = _TexDivideX;
                const uint dy = _TexDivideY;
                const float tdx = 1.0 / dx;
                const float tdy = 1.0 / dy;
                uint index = (uint)fmod(_Time.y * 30.0, dx * dy);
                uint ix = index % dx;
                uint iy = (dy - 1) - (index / dx);
                o.texcoord = float2(v.texcoord.x, v.texcoord.y) * float2(tdx, tdy);
                o.texcoord.x += ix * tdx;
                o.texcoord.y += iy * tdy;

                o.pos = UnityObjectToClipPos(v.vertex);
                float3 posWS = mul(unity_ObjectToWorld, v.vertex).xyz;
#if defined(BILLBOARD)
                float3 normalWS = -unity_CameraToWorld._m02_m12_m22;
                float3 tangentWS = unity_CameraToWorld._m00_m10_m20;
                float3 bitangentWS = unity_CameraToWorld._m01_m11_m21;
#else
                fixed3 normalWS = UnityObjectToWorldNormal(v.normal);
                fixed3 tangentWS = UnityObjectToWorldDir(v.tangent.xyz);
                fixed3 bitangentWS = cross(normalWS, tangentWS) * v.tangent.w;
#endif
                float3x3 mtxWSToTS = float3x3(tangentWS, bitangentWS, normalWS);

                // DirectionalLight
                float3 lightDirWS = _WorldSpaceLightPos0.xyz;
                o.lightDirTS = mul(mtxWSToTS, lightDirWS);
                o.lightColor = _LightColor0.rgb;
            
                // PointLights
                float3 lightPos = float3(0,0,0);
                uint lightCount = 0;
                [unroll]
                for (uint i = 0; i < 4; i++)
                {
                    lightPos += float3(unity_4LightPosX0[i], unity_4LightPosY0[i], unity_4LightPosZ0[i]);
                    lightCount += (unity_LightColor[i].a > 0) ? 1 : 0;
                }
                lightPos /= lightCount;

                float3 lightDirWS2 = normalize(lightPos - posWS.xyz); 
                o.lightDirTS2 = mul(mtxWSToTS, lightDirWS2);
                o.lightColor2 = FourPointLightsColor(
                  unity_4LightPosX0,
                  unity_4LightPosY0,
                  unity_4LightPosZ0,
                  unity_LightColor[0].rgb,
                  unity_LightColor[1].rgb,
                  unity_LightColor[2].rgb,
                  unity_LightColor[3].rgb,
                  unity_4LightAtten0,
                  posWS);
                return o;
            }

            half4 frag(v2f In) : COLOR
            {
                half4 colPositive = tex2D(_PisitiveTex, In.texcoord);
                half4 colNegative = tex2D(_NegativeTex, In.texcoord);

                // DirectionalLight
                float3 lightDirTS = normalize(In.lightDirTS);
                float lightmap01 = ComputeLightMap(lightDirTS, colPositive, colNegative);
                // PointLights
                float3 lightDirTS2 = normalize(In.lightDirTS2);
                float lightmap02 = ComputeLightMap(lightDirTS2, colPositive, colNegative);

                half3 rgb01 = lightmap01 * In.lightColor;
                half3 rgb02 = lightmap02 * In.lightColor2;
                half3 rgb = rgb01 + rgb02;

                half4 col = half4(rgb, colPositive.a);
                col.rgb += _EmissiveColor.rgb * colNegative.a;
                return col;
            }
            ENDCG
        }
    }
}

参考

Visual Effect Graph の 6 ウェイライティングを使ったリアルな煙のライティング | Unity Blog

GitHub - keijiro/SixWaySmokeTest

6-way lightmap WIP - #20 by Aaron - Real Time VFX

この記事はFrontとBackに近似値を使用してチャンネルを節約したものを解説している?

Smoke Lighting and texture re-usability in Skull & Bones - Real Time VFX

https://github.com/peeweek/Unity-URP-SmokeLighting

Houdini ApprenticeでVAT出力

環境

Houdini Apprentice 19.5.534

python3.9 Fbx Sdk

概要

Houdiniでパーティクルを独自VATとしてExr形式のファイルに出力します

Unity上で再生できるプログラムを作ったところ、同じように動作しました

パーティクルは参考の動画をみて作成しました。バージョンが上がり多少違いましたが、同じような感じに出来ました

また動画はライフに基づいてカラーを0に近づけていますが、こちらはAlphaのアトリビュートを追加してそれに対して行っています

Alphaというアトリビュートを追加すると勝手にアルファブレンドになる模様

import OpenEXR
import numpy as np
import Imath
import json

fps = int(hou.fps())
node = hou.pwd()
targetNode = hou.node(node.parm("node").eval())
startFrame = node.parm("startFrame").eval()
endFrame = node.parm("endFrame").eval()
imageWidth = node.parm("imageWidth").eval()
imageHeight = node.parm("imageHeight").eval()
outputDir = node.parm("outputDir").eval()

imageSize = imageWidth * imageHeight
channelNum = 4
strideTexelNum = 2
pixelType = Imath.PixelType.HALF
dataType = np.float16
texels = np.zeros((imageSize, channelNum), dtype=dataType)
texIndex = 0
texelIndex = 0

def writeImage():
    header = OpenEXR.Header(imageWidth, imageHeight)
    header['channels'] = {
        'R': Imath.Channel(Imath.PixelType(pixelType)),
        'G': Imath.Channel(Imath.PixelType(pixelType)),
        'B': Imath.Channel(Imath.PixelType(pixelType)),
        'A': Imath.Channel(Imath.PixelType(pixelType))
    }
    header['compression'] =Imath.Compression(Imath.Compression.NO_COMPRESSION)
    outputPath = f'{outputDir}/output{texIndex}.exr';
    print(outputPath)
    output = OpenEXR.OutputFile(outputPath, header)
    output.writePixels({
        'R': texels[:,0].astype(dataType).tostring(),
        'G': texels[:,1].astype(dataType).tostring(),
        'B': texels[:,2].astype(dataType).tostring(),
        'A': texels[:,3].astype(dataType).tostring()
    })
    output.close()
    
def writePoints(frame, maxPointNum):
    global texels, texIndex, texelIndex
    
    geo = targetNode.geometry()
    pointNum = len(geo.points())
    texelNum = maxPointNum * strideTexelNum
    if (texelIndex + texelNum) > imageSize:
        writeImage()
        texIndex += 1
        texelIndex = 0
        texels[:,] = [0.0, 0.0, 0.0, 0.0]

    for point in geo.points():
        pos = point.position()
        texels[texelIndex + 0,] = [pos.x(), pos.y(), pos.z(), 0.0]
        color = point.attribValue('Cd')
        alpha = point.attribValue('Alpha')
        texels[texelIndex + 1,] = [color[0], color[1], color[2], alpha]
        texelIndex += strideTexelNum

    dummyNum = maxPointNum - pointNum
    for i in range(dummyNum):
        texels[texelIndex + 0,] = [0.0, 0.0, 0.0, 0.0]
        texels[texelIndex + 1,] = [0.0, 0.0, 0.0, 0.0]
        texelIndex += strideTexelNum

def main():
    global texIndex, texelIndex
    
    maxPointNum = 0
    frameNum = (endFrame - startFrame) + 1
    for i in range(frameNum):
        frame = startFrame + i
        hou.setFrame(frame)
        geo = targetNode.geometry()
        maxPointNum = max(maxPointNum, len(geo.points()))

    texIndex = 0
    texelIndex = 0
    for i in range(frameNum):
        frame = startFrame + i
        hou.setFrame(frame)
        writePoints(frame, maxPointNum)
    if texelIndex > 0 :
        writeImage()
        texIndex += 1
    
    paramDict = {}
    paramDict['texNum'] = texIndex;
    paramDict['fps'] = fps
    paramDict['frameNum'] = frameNum;
    paramDict['maxPointNumInFrame'] = maxPointNum;
    jsonData = json.dumps(paramDict, ensure_ascii=False)
    print(jsonData)
    outputPath = f'{outputDir}/output.json';
    f = open(outputPath, 'w')
    f.write(jsonData)
    f.close()

main()

参考

Houdini ダイナミクス基本講座 Part1:ダイナミクスシミュレーションの流れ - YouTube

プリミティブ版

HdrpVatExample/Fluid.hip at master · keijiro/HdrpVatExample · GitHub

に三角形分割、Color、Normal、UVを追加したものを使用してテスト

import OpenEXR
import numpy as np
import Imath
import json

fps = int(hou.fps())
node = hou.pwd()
targetNode = hou.node(node.parm("node").eval())
startFrame = node.parm("startFrame").eval()
endFrame = node.parm("endFrame").eval()
imageWidth = node.parm("imageWidth").eval()
imageHeight = node.parm("imageHeight").eval()
outputDir = node.parm("outputDir").eval()

imageSize = imageWidth * imageHeight
channelNum = 4
strideTexelNum = 3
pixelType = Imath.PixelType.HALF
dataType = np.float16
texels = np.zeros((imageSize, channelNum), dtype=dataType)
texIndex = 0
texelIndex = 0
pointInface = 3

def writeImage():
    header = OpenEXR.Header(imageWidth, imageHeight)
    header['channels'] = {
        'R': Imath.Channel(Imath.PixelType(pixelType)),
        'G': Imath.Channel(Imath.PixelType(pixelType)),
        'B': Imath.Channel(Imath.PixelType(pixelType)),
        'A': Imath.Channel(Imath.PixelType(pixelType))
    }
    header['compression'] =Imath.Compression(Imath.Compression.NO_COMPRESSION)
    outputPath = f'{outputDir}/output{texIndex}.exr';
    print(outputPath)
    output = OpenEXR.OutputFile(outputPath, header)
    output.writePixels({
        'R': texels[:,0].astype(dataType).tostring(),
        'G': texels[:,1].astype(dataType).tostring(),
        'B': texels[:,2].astype(dataType).tostring(),
        'A': texels[:,3].astype(dataType).tostring()
    })
    output.close()
    
def writeFaces(frame, maxPointNum):
    global texels, texIndex, texelIndex
    
    geo = targetNode.geometry()
    texelNum = maxPointNum * strideTexelNum
    pointNum = len(geo.prims()) * pointInface
    if pointNum > maxPointNum:
        print(f'error pointNum:{pointNum} maxPointNum:{maxPointNum}')
        return
    
    if (texelIndex + texelNum) > imageSize:
        writeImage()
        texIndex += 1
        texelIndex = 0
        texels[:,] = [0.0, 0.0, 0.0, 0.0]

    for prim in geo.prims():
        #print(f'prim{prim.number()}')
        if len(prim.vertices()) != pointInface:
            print('error verticesNum')
            break
        pointInPrim = []
        for vertex in prim.vertices():
            pointInPrim.append(vertex.point())
        if len(pointInPrim) != pointInface:
            print('error pointInPrim')
            break
        for i in range(pointInface):
            point = pointInPrim[int((pointInface - i) % pointInface)]
            pos = point.position()
            #print(f'pos{index}/{point.number()}:{pos}')
            normal = point.attribValue('N')
            color = point.attribValue('Cd')
            alpha = point.attribValue('Alpha')
            uv = point.attribValue('uv')
            texels[texelIndex + 0,] = [pos.x(), pos.y(), pos.z(), uv[0]]
            texels[texelIndex + 1,] = [normal[0], normal[1], normal[2], uv[1]]
            texels[texelIndex + 2,] = [color[0], color[1], color[2], alpha]
            texelIndex += strideTexelNum
        #break

    dummyNum = maxPointNum - pointNum
    for i in range(dummyNum):
        texels[texelIndex + 0,] = [0.0, 0.0, 0.0, 0.0]
        texels[texelIndex + 1,] = [0.0, 0.0, 0.0, 0.0]
        texels[texelIndex + 2,] = [0.0, 0.0, 0.0, 0.0]
        texelIndex += strideTexelNum

def main():
    global texIndex, texelIndex
    
    frameNum = (endFrame - startFrame) + 1
    geo = targetNode.geometry()
    maxPointNum = 0
    frameNum = (endFrame - startFrame) + 1
    for i in range(frameNum):
        frame = startFrame + i
        hou.setFrame(frame)
        geo = targetNode.geometry()
        maxPointNum = max(maxPointNum, len(geo.prims()) * pointInface)

    texIndex = 0
    texelIndex = 0
    for i in range(frameNum):
        frame = startFrame + i
        hou.setFrame(frame)
        print(f'frame:{frame}')
        writeFaces(frame, maxPointNum)
    if texelIndex > 0 :
        writeImage()
        texIndex += 1
    
    paramDict = {}
    paramDict['texNum'] = texIndex;
    paramDict['fps'] = fps
    paramDict['frameNum'] = frameNum;
    paramDict['maxPointNumInFrame'] = maxPointNum;
    paramDict['strideTexelNum'] = strideTexelNum;
    jsonData = json.dumps(paramDict, ensure_ascii=False)
    print(jsonData)
    outputPath = f'{outputDir}/output.json';
    f = open(outputPath, 'w')
    f.write(jsonData)
    f.close()

main()
print('finish')

Houdini ApprenticeでFbx出力

環境

Houdini Apprentice 19.5.534

python3.9 Fbx Sdk

概要

FbxSdkを使用してGeometryを出力してみました。

位置と法線のみです。

Normalノードを通さないと法線も取れないの?

import FbxCommon
from fbx import *

node = hou.pwd()
geo = node.geometry()

def CreateGeometry(pSdkManager, pName):
    mesh = FbxMesh.Create(pSdkManager,pName)
    layer = mesh.GetLayer(0)
    if layer == None:
        mesh.CreateLayer()
        layer = mesh.GetLayer(0)

    positions = []
    for point in geo.points():
        pos = point.position()
        positions.append(FbxVector4(pos.x(), pos.y(), pos.z()))

    attrNormal = geo.findPointAttrib('N')
    normals = []
    for point in geo.points():
        normal = point.attribValue(attrNormal)
        normals.append(FbxVector4(normal[0], normal[1], normal[2]))

    mesh.InitControlPoints(len(positions))
    i = 0
    for position in positions:
      mesh.SetControlPointAt(position, i)
      i+=1
 
    layerElemNormal = FbxLayerElementNormal.Create(mesh, "")
    layerElemNormal.SetMappingMode(FbxLayerElement.eByControlPoint)
    layerElemNormal.SetReferenceMode(FbxLayerElement.eDirect)
    for normal in normals:
        layerElemNormal.GetDirectArray().Add(normal)
    layer.SetNormals(layerElemNormal)

    for prim in geo.prims():
        mesh.BeginPolygon(-1, -1, False)
        indicesInPrim = []
        for vertex in prim.vertices():
            indicesInPrim.append(vertex.point().number())
        indexNum = len(indicesInPrim)
        for i in range(indexNum):
            index = int((indexNum - i) % indexNum)
            mesh.AddPolygon(indicesInPrim[index])
        mesh.EndPolygon()

    node = FbxNode.Create(pSdkManager,pName)
    node.SetNodeAttribute(mesh)
    node.SetShadingMode(FbxNode.eTextureShading)
    return node

def CreateScene(pSdkManager, pScene, pSampleFileName):
    lCube = CreateGeometry(pSdkManager, "Geo")
    lRootNode = pScene.GetRootNode()
    lRootNode.AddChild(lCube)

def main():
    lSampleFileName = "D:/user/houdini/outputgeo.fbx"
    (lSdkManager, lScene) = FbxCommon.InitializeSdkObjects()
    lResult = CreateScene(lSdkManager, lScene, lSampleFileName)
    lResult = FbxCommon.SaveScene(lSdkManager, lScene, lSampleFileName)
    lSdkManager.Destroy()

main()