Unity 2Dレンダラで3Dオブジェクトをライティングする
qiita.com
ノーマルマップがないモデルだといい感じにライティングされなかったからこれ多分モデルにノーマルマップがあるときの事例だと思う(未検証)
頂点のノーマルだけで事足りる場合もあるのでちょっと改変した
〜 shader "Universal Render Pipeline/2D/Sprite-Lit-3D" { Properties { [MainTexture] _MainTex("Diffuse", 2D) = "white" {} [MainColor] _BaseColor("Color", Color) = (1,1,1,1) [HDR] _EmissionColor("Emission", Color) = (0,0,0,0) _MaskTex("Mask", 2D) = "white" {} _NormalMap("Normal Map", 2D) = "bump" {} // Legacy properties. They're here so that materials using this shader can gracefully fallback to the legacy sprite shader. [HideInInspector] _Color("Tint", Color) = (1,1,1,1) [HideInInspector] _RendererColor("RendererColor", Color) = (1,1,1,1) [HideInInspector] _Flip("Flip", Vector) = (1,1,1,1) [HideInInspector] _AlphaTex("External Alpha", 2D) = "white" {} [HideInInspector] _EnableExternalAlpha("Enable External Alpha", Float) = 0 } HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" ENDHLSL SubShader { Tags {"Queue" = "Geometry" "RenderType" = "Opacue" "RenderPipeline" = "UniversalPipeline" } Blend SrcAlpha OneMinusSrcAlpha Cull Back ZWrite On Pass { Tags { "LightMode" = "Universal2D" } HLSLPROGRAM #pragma prefer_hlslcc gles #pragma vertex CombinedShapeLightVertex #pragma fragment CombinedShapeLightFragment #pragma multi_compile USE_SHAPE_LIGHT_TYPE_0 __ #pragma multi_compile USE_SHAPE_LIGHT_TYPE_1 __ #pragma multi_compile USE_SHAPE_LIGHT_TYPE_2 __ #pragma multi_compile USE_SHAPE_LIGHT_TYPE_3 __ struct Attributes { float3 positionOS : POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; float2 lightingUV : TEXCOORD1; }; #include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/LightingUtility.hlsl" TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); TEXTURE2D(_MaskTex); SAMPLER(sampler_MaskTex); TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); half4 _MainTex_ST; half4 _NormalMap_ST; half4 _BaseColor; half4 _EmissionColor; #if USE_SHAPE_LIGHT_TYPE_0 SHAPE_LIGHT(0) #endif #if USE_SHAPE_LIGHT_TYPE_1 SHAPE_LIGHT(1) #endif #if USE_SHAPE_LIGHT_TYPE_2 SHAPE_LIGHT(2) #endif #if USE_SHAPE_LIGHT_TYPE_3 SHAPE_LIGHT(3) #endif Varyings CombinedShapeLightVertex(Attributes v) { Varyings o = (Varyings)0; o.positionCS = TransformObjectToHClip(v.positionOS); o.uv = TRANSFORM_TEX(v.uv, _MainTex); float4 clipVertex = o.positionCS / o.positionCS.w; o.lightingUV = ComputeScreenPos(clipVertex).xy; o.color = v.color * _BaseColor; return o; } #include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/CombinedShapeLightShared.hlsl" half4 CombinedShapeLightFragment(Varyings i) : SV_Target { half4 main = i.color * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv); half4 mask = SAMPLE_TEXTURE2D(_MaskTex, sampler_MaskTex, i.uv); return CombinedShapeLightShared(main, mask, i.lightingUV)+main*_EmissionColor; } ENDHLSL } Pass { Tags { "LightMode" = "NormalsRendering"} HLSLPROGRAM #pragma prefer_hlslcc gles #pragma vertex NormalsRenderingVertex #pragma fragment NormalsRenderingFragment struct Attributes { float3 positionOS : POSITION; float3 normalOS : NORMAL;//ここで頂点の法線を取得 float4 color : COLOR; float2 uv : TEXCOORD0; float4 tangent : TANGENT; }; struct Varyings { float4 positionCS : SV_POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; float3 normalWS : TEXCOORD1; float3 tangentWS : TEXCOORD2; float3 bitangentWS : TEXCOORD3; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); float4 _NormalMap_ST; // Is this the right way to do this? Varyings NormalsRenderingVertex(Attributes attributes) { Varyings o = (Varyings)0; o.positionCS = TransformObjectToHClip(attributes.positionOS); o.uv = TRANSFORM_TEX(attributes.uv, _NormalMap); o.uv = attributes.uv; o.color = attributes.color; o.normalWS = TransformObjectToWorldDir(attributes.normalOS); o.tangentWS = TransformObjectToWorldDir(attributes.tangent.xyz); o.bitangentWS = cross(o.normalWS, o.tangentWS) * attributes.tangent.w; return o; } #include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/NormalsRenderingShared.hlsl" float4 NormalsRenderingFragment(Varyings i) : SV_Target { float4 mainTex = i.color * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv); float3 normalTS = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, i.uv)); //return NormalsRenderingShared(mainTex, normalTS, i.tangentWS.xyz, i.bitangentWS.xyz, i.normalWS.xyz); half3 normalVS = TransformWorldToViewDir(i.normalWS.xyz);//頂点の法線情報をそのまま使う half4 normalColor; normalColor.rgb = 0.5 * ((normalVS)+1); normalColor.a = i.color.a; // used for blending return normalColor; } ENDHLSL } Pass { Tags { "LightMode" = "UniversalForward" "Queue"="Geometry" "RenderType"="Opacue"} HLSLPROGRAM #pragma prefer_hlslcc gles #pragma vertex UnlitVertex #pragma fragment UnlitFragment struct Attributes { float3 positionOS : POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float4 _MainTex_ST; Varyings UnlitVertex(Attributes attributes) { Varyings o = (Varyings)0; o.positionCS = TransformObjectToHClip(attributes.positionOS); o.uv = TRANSFORM_TEX(attributes.uv, _MainTex); o.uv = attributes.uv; o.color = attributes.color; return o; } float4 UnlitFragment(Varyings i) : SV_Target { float4 mainTex = i.color * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv); return mainTex; } ENDHLSL } } FallBack "Universal Render Pipeline/Unlit" } 〜
Tags { "LightMode" = "NormalsRendering"}
のPassで
頂点シェーダで法線情報をもらって
NormalsRenderingFragmentでそのままつかっただけ
白いCubeは3dモデルだけど松明のLight2D(PointLight)の光を受けている
パストレ
#### PBR本とソースコード
ソースコード github.com
#### パストレ確率論解説
uraymarchingメモ
- http://tips.hecomi.com/entry/2018/12/31/211448
- GitHub - hecomi/uRaymarching: Raymarching Shader Generator in Unity
- Raymarching.cginc
コード例
- レンダリングパスでスイッチングする部分(https://github.com/hecomi/uRaymarching/blob/a2574b027203b4388234679dcb567dcc9ecc21af/Assets/uRaymarching/Shaders/Include/Legacy/DeferredStandard.cginc)
#ifndef VERT_FRAG_DEFERRED_OBJECT_STANDARD_H #define VERT_FRAG_DEFERRED_OBJECT_STANDARD_H #include "UnityCG.cginc" #include "Lighting.cginc" #include "UnityPBSLighting.cginc" #include "./Structs.cginc" #include "./Raymarching.cginc" #include "./Utils.cginc" int _Loop; float _MinDistance; fixed4 _Color; float _Glossiness; float _Metallic; #ifdef FULL_SCREEN struct v2f { float4 pos : SV_POSITION; float4 projPos : TEXCOORD0; float4 lmap : TEXCOORD1; #ifdef LIGHTMAP_OFF #if UNITY_SHOULD_SAMPLE_SH half3 sh : TEXCOORD2; #endif #endif }; #else struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD0; float3 worldNormal : TEXCOORD1; float4 projPos : TEXCOORD2; float4 lmap : TEXCOORD3; #ifdef LIGHTMAP_OFF #if UNITY_SHOULD_SAMPLE_SH half3 sh : TEXCOORD4; #endif #endif }; #endif v2f Vert(appdata_full v) { v2f o; #ifdef FULL_SCREEN o.pos = v.vertex; #else o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); #endif o.projPos = ComputeNonStereoScreenPos(o.pos); COMPUTE_EYEDEPTH(o.projPos.z); #ifndef DYNAMICLIGHTMAP_OFF o.lmap.zw = v.texcoord2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw; #else o.lmap.zw = 0; #endif #ifndef LIGHTMAP_OFF o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw; #else o.lmap.xy = 0; #ifndef SPHERICAL_HARMONICS_PER_PIXEL #if UNITY_SHOULD_SAMPLE_SH o.sh = 0; o.sh = ShadeSHPerVertex(o.worldNormal, o.sh); #endif #endif #endif return o; } GBufferOut Frag(v2f i, GBufferOut o) { RaymarchInfo ray; INITIALIZE_RAYMARCH_INFO(ray, i, _Loop, _MinDistance); Raymarch(ray); #ifdef USE_RAYMARCHING_DEPTH o.depth = ray.depth; #endif float3 worldPos = ray.endPos; float3 worldNormal = 2.0 * ray.normal - 1.0; fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos)); SurfaceOutputStandard so; UNITY_INITIALIZE_OUTPUT(SurfaceOutputStandard, so); so.Albedo = _Color.rgb; so.Metallic = _Metallic; so.Smoothness = _Glossiness; so.Emission = 0.0; so.Alpha = _Color.a; so.Occlusion = 1.0; so.Normal = worldNormal; #ifdef POST_EFFECT POST_EFFECT(ray, so); #endif UnityGI gi; UNITY_INITIALIZE_OUTPUT(UnityGI, gi); gi.indirect.diffuse = 0; gi.indirect.specular = 0; gi.light.color = 0; gi.light.dir = half3(0, 1, 0); gi.light.ndotl = LambertTerm(worldNormal, gi.light.dir); UnityGIInput giInput; UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput); giInput.light = gi.light; giInput.worldPos = worldPos; giInput.worldViewDir = worldViewDir; giInput.atten = 1; #if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON) giInput.lightmapUV = i.lmap; #else giInput.lightmapUV = 0.0; #endif #if UNITY_SHOULD_SAMPLE_SH #ifdef SPHERICAL_HARMONICS_PER_PIXEL giInput.ambient = ShadeSHPerPixel(worldNormal, 0.0, worldPos); #else giInput.ambient.rgb = i.sh; #endif #else giInput.ambient.rgb = 0.0; #endif giInput.probeHDR[0] = unity_SpecCube0_HDR; giInput.probeHDR[1] = unity_SpecCube1_HDR; #if UNITY_SPECCUBE_BLENDING || UNITY_SPECCUBE_BOX_PROJECTION giInput.boxMin[0] = unity_SpecCube0_BoxMin; // .w holds lerp value for blending #endif #if UNITY_SPECCUBE_BOX_PROJECTION giInput.boxMax[0] = unity_SpecCube0_BoxMax; giInput.probePosition[0] = unity_SpecCube0_ProbePosition; giInput.boxMax[1] = unity_SpecCube1_BoxMax; giInput.boxMin[1] = unity_SpecCube1_BoxMin; giInput.probePosition[1] = unity_SpecCube1_ProbePosition; #endif LightingStandard_GI(so, giInput, gi); o.emission = LightingStandard_Deferred(so, worldViewDir, gi, o.diffuse, o.specular, o.normal); #ifndef UNITY_HDR_ON o.emission.rgb = exp2(-o.emission.rgb); #endif UNITY_OPAQUE_ALPHA(o.diffuse.a); return o; } #endif
- エディタからコード生成されるもの(生成元はhttps://github.com/hecomi/uRaymarching/blob/a2574b027203b4388234679dcb567dcc9ecc21af/Assets/uRaymarching/Editor/Resources/ShaderTemplates/uRaymarching/Deferred/Standard.txt )
Shader "Raymarching/Forward_HexFloor" { Properties { [Header(PBS)] _Color("Color", Color) = (1.0, 1.0, 1.0, 1.0) _Metallic("Metallic", Range(0.0, 1.0)) = 0.5 _Glossiness("Smoothness", Range(0.0, 1.0)) = 0.5 [Header(Pass)] [Enum(UnityEngine.Rendering.CullMode)] _Cull("Culling", Int) = 2 [Header(Raymarching)] _Loop("Loop", Range(1, 100)) = 30 _MinDistance("Minimum Distance", Range(0.001, 0.1)) = 0.01 _DistanceMultiplier("Distance Multiplier", Range(0.001, 2.0)) = 1.0 [PowerSlider(10.0)] _NormalDelta("NormalDelta", Range(0.00001, 0.1)) = 0.0001 // @block Properties [Header(Additional Properties)] _TopColor("TopColor", Color) = (1, 1, 1, 0) // @endblock } SubShader { Tags { "RenderType" = "Opaque" "Queue" = "Geometry" "DisableBatching" = "True" } Cull [_Cull] CGINCLUDE #define OBJECT_SHAPE_CUBE #define USE_RAYMARCHING_DEPTH #define SPHERICAL_HARMONICS_PER_PIXEL #define DISTANCE_FUNCTION DistanceFunction #define POST_EFFECT PostEffect #define PostEffectOutput SurfaceOutputStandard #include "Assets\uRaymarching\Shaders\Include\Legacy/Common.cginc" // @block DistanceFunction inline float DistanceFunction(float3 pos) { // combine even hex tiles and odd hex tiles float radius = 0.2; float space = 0.1; float wave = 0.1; float3 objectScale = GetScale(); float height = objectScale.y * 0.5 - wave; float3 scale = objectScale * 0.5; float pitch = radius * 2 + space; float3 offset = float3(pitch * 0.5, 0.0, pitch * 0.866); float3 loop = float3(offset.x * 2, 1.0, offset.z * 2); float3 p1 = pos; float3 p2 = pos + offset; // calculate indices float2 pi1 = floor(p1 / loop).xz; float2 pi2 = floor(p2 / loop).xz; pi1.y = pi1.y * 2 + 1; pi2.y = pi2.y * 2; p1 = Repeat(p1, loop); p2 = Repeat(p2, loop); // draw hexagonal prisms with random heights float dy1 = wave * sin(10 * Rand(pi1) + 5 * PI * _Time.x); float dy2 = wave * sin(10 * Rand(pi2) + 5 * PI * _Time.x); float d1 = HexagonalPrismY(float3(p1.x, pos.y + dy1, p1.z), float2(radius, height)); float d2 = HexagonalPrismY(float3(p2.x, pos.y + dy2, p2.z), float2(radius, height)); // maximum indices loop.z *= 0.5; float2 mpi1 = floor((scale.xz + float2(space * 0.5, radius)) / loop.xz); float2 mpi2 = floor((scale.xz + float2(radius + space, radius)) / loop.xz); // remove partial hexagonal prisms // if (pi1.x >= mpi1.x || pi1.x < -mpi1.x) d1 = max(d1, space); // if (pi1.y >= mpi1.y || pi1.y <= -mpi1.y) d1 = max(d1, space); float o1 = any( step(mpi1.x, pi1.x) + step(pi1.x + 1, -mpi1.x) + step(mpi1.y, abs(pi1.y))); d1 = o1 * max(d1, 0.1) + (1 - o1) * d1; // if (!all(max(mpi2 - abs(pi2), 0.0))) d2 = max(d2, space); float o2 = any(step(mpi2, abs(pi2))); d2 = o2 * max(d2, 0.1) + (1 - o2) * d2; // combine //return min(d1, d2); return d1; } // @endblock // @block PostEffect float4 _TopColor; inline void PostEffect(RaymarchInfo ray, inout PostEffectOutput o) { float3 localPos = ToLocal(ray.endPos); o.Emission += smoothstep(0.48, 0.50, localPos.y) * _TopColor; o.Occlusion *= 1.0 - 1.0 * ray.loop / ray.maxLoop; } // @endblock ENDCG Pass { Tags { "LightMode" = "Deferred" } Stencil { Comp Always Pass Replace Ref 128 } CGPROGRAM #include "Assets\uRaymarching\Shaders\Include\Legacy/DeferredStandard.cginc" #pragma target 3.0 #pragma vertex Vert #pragma fragment Frag #pragma exclude_renderers nomrt #pragma multi_compile_prepassfinal #pragma multi_compile ___ UNITY_HDR_ON ENDCG } } Fallback "Raymarching/Fallbacks/StandardSurfaceShader" CustomEditor "uShaderTemplate.MaterialEditor" }
仕組み
- vertexシェーダでprojPosにComputeNonStereoScreenPosでスクリーン上の座標を入れとくことで後のポストエフェクトに使いやすくしている(利便性のためzはEYE_DEPTHを適用してビュー空間上の深度に変換している。)
- PostEffect関数で返すべきデータの種類がレンダリングモードに応じて適切に選択できるように、PostEffectOutputをdefineしている(どう変わるかはhttps://github.com/hecomi/uRaymarching/blob/master/Documents/Legacy.md のPostEffectを参照)
CGのノート
Wについて
シェーダを触るとき頂点情報にWがついている。Wの役目をまとめる。
1. 3次元空間の頂点に対する変形行列のため
wの次元があることで3x3行列では表せられない平行移動を4x4行列で表すことができる。MV行列まではこの役割しかない。
2. 透視投影をするときの深度除算をするためにwにカメラからの距離(=ビュー空間でのz座標)を保存しておく。
http://marupeke296.com/DXG_No70_perspective.html
3. ラスタライザをするときに、頂点情報をパースペクティブコレクトに補間するための深度情報としてもっておく。
かなり調査に時間をかけた。
ラスタライザをするときに、線分上や面上の法線情報やUV座標情報は、その線分や面を構成する頂点に付加された情報で補間しなければならない。しかし、画面上の位置関係で補間すると遠近間が正しく反映されないので歪む。これはPS1などの時代にあった問題であり、ポリゴン分割を工夫するなどでごまかしていた*1*2
これを解決するための方法を示した論文がある
https://www.comp.nus.edu.sg/~lowkl/publications/lowk_persp_interp_techrep.pdf
具体的には、頂点A,Bがスクリーン上に映っているとき、AB間をスクリーン上でs,1-sで内分する点Tでの頂点情報を補間によってもとめるには
1. Tでの(クリッピング空間での)深度Ztを求める(論文中の(1)から(12)まで)。
2. Tでの頂点情報Itを求める。
2についてはZtを経由せずに求めることもできる
2式は変形でき,「頂点情報については、頂点情報を深度で割ったものでスクリーン上の線形補間を行えばよい」と解釈できる。
実際の実装については
社内勉強会「レンダリング合宿1」 ソフトウェアラスタライザー基本実装編 07.パースペクティブコレクト - Qiita
が参考になりました。
「HLSLのSV_Positionや、OpenGLのgl_Positionは、XYZ要素がW除算され、NDC空間の値としてフラグメントシェーダに渡される。このときはスクリーン上の線形補間がされる(式1)」
「HLSLのTEXCOORDや、OpenGLのvarying変数などは、パースペクティブ補間され(式2)、View空間での値としてフラグメントシェーダに渡される」
深度値と頂点情報のラスタライズの計算の違い
zバッファには、「ビュー空間での深度=Zview」ではなく「射影変換した後(=正規化デバイス空間(NDC))での深度=Zndc」が入っている。このため、「物体の現実世界の奥行きの距離(=Zview)とZndcの値には線形関係がない」単純な発想として、zバッファに「Zviewの値を0~1にスケールしたもの」を使えばよさそうである。しかし、Zndcの値を使うには理由がある。
1. 「物体の前後関係を調べるだけならZndcの大小関係の情報だけでよく、線形性は必要でない」ため
2. near平面からfar平面に分布するZndcの値が非線形に分布し、nearに多く分布する。カメラに近いほど深度の精度を要求したいという要求にあったため。
しかし、「zバッファに浮動小数点を使い、かつnearに0、farに1を割り当てた」場合、Zndcの非線形分布と浮動小数点数のレンジの関係で、遠い面での深度の精度が落ちてしまうという問題があった。
これを改善するためにreversed z sortが提案された
Depth Precision Visualized | NVIDIA Developer
3. ラスタライズするときに、Zndcはスクリーン上の線形補間だけで計算できるため(というか、Zndcは正規化デバイス空間での値なので、パースペクティブコレクトとかを考える必要がない)。
具体的に言うと、zバッファを作る際ピクセルシェーダに渡されたSV_Positionやgl_FragCoordのZ要素をそのまま使えるということである。特別な処理が必要でないので、zバッファはOpenGLやDirectXでは設定一つで勝手に作ってくれる。
たとえばこの例*3では、zバッファを使ったシャドウマッピングを行う際(depthBuffer == Trueのとき)、深度バッファの値としてピクセルシェーダに渡されたgl_FragCoord.zをgl_FragColorに直接代入している。なぜならgl_Positionは勝手にW除算されてgl_FragCoordに渡されるため、フラグメントシェーダに渡されるZ座標がそのままZndcを示しているからだ。
(記事では,zバッファにFragCoord.zを入れた場合(depthBuffer == Trueの場合)と、ビュー空間での深度(varyingで頂点情報を保存してフラグメントシェーダに渡す。式2に該当)(depthBuffer == Falseの場合)を代入した場合で場合分けしている。場合分け変数名がdepthBufferなのは違和感がある...おそらく「FragCoord.zはOpenGLが勝手に作成したzバッファのz値と一致していて、それを利用している」という意味であるとおもう。depthBuffer==Trueのとき、「シャドウマッピングのための深度バッファはOpenGLの作ったzバッファの値を使う。(直接OpenGLのzバッファにアクセスしているわけではない)」。depthBuffer == falseのとき、「シャドウマッピングのための深度バッファは頂点情報から自前で計算したZviewの値を使う」という意味で考えてそう)