Cg Programming/Unity/Two-Sided Surfaces
This tutorial covers two-sided per-vertex lighting.
It's part of a series of tutorials about basic lighting in Unity. In this tutorial, we extend Section “Specular Highlights” to render two-sided surfaces. If you haven't read Section “Specular Highlights”, this would be a very good time to read it.
Two-Sided Lighting
As shown by the figure of the algebraic surface, it's sometimes useful to apply different colors to the two sides of a surface. In Section “Cutaways”, we have seen how two passes with front face culling and back face culling can be used to apply different shaders to the two sides of a mesh. We will apply the same strategy here.
As mentioned in Section “Cutaways”, an alternative approach in Cg is to use a fragment input parameter with semantic FACE, VFACE, or SV_IsFrontFacing (depending on the API) to distinguish between the two sides, however, this doesn't seem to work in Unity.
Shader Code
The shader code for two-sided per-vertex lighting is a straightforward extension of the code in Section “Specular Highlights”. It requires two sets of material parameters (front and back) and duplicates all passes — one copy with front-face culling and the other with back-face culling. The shaders of the two copies are identical except that the shader for the back faces uses the negated surface normal vector and the properties for the back material.
Shader "Cg two-sided per-vertex lighting" { Properties { _Color ("Front Material Diffuse Color", Color) = (1,1,1,1) _SpecColor ("Front Material Specular Color", Color) = (1,1,1,1) _Shininess ("Front Material Shininess", Float) = 10 _BackColor ("Back Material Diffuse Color", Color) = (1,1,1,1) _BackSpecColor ("Back Material Specular Color", Color) = (1,1,1,1) _BackShininess ("Back Material Shininess", Float) = 10 } SubShader { Pass { Tags { "LightMode" = "ForwardBase" } // pass for ambient light and first light source Cull Back // render only front faces CGPROGRAM #pragma vertex vert #pragma fragment frag // User-specified properties uniform float4 _Color; uniform float4 _SpecColor; uniform float _Shininess; uniform float4 _BackColor; uniform float4 _BackSpecColor; uniform float _BackShininess; // The following built-in uniforms (apart from _LightColor0) // are defined in "UnityCG.cginc", which could be #included uniform float4 unity_Scale; // w = 1/scale; see _World2Object uniform float3 _WorldSpaceCameraPos; uniform float4x4 _Object2World; // model matrix uniform float4x4 _World2Object; // inverse model matrix // (all but the bottom-right element have to be scaled // with unity_Scale.w if scaling is important) uniform float4 _WorldSpaceLightPos0; // position or direction of light source uniform float4 _LightColor0; // color of light source (from "Lighting.cginc") struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; }; struct vertexOutput { float4 pos : SV_POSITION; float4 col : COLOR; }; vertexOutput vert(vertexInput input) { vertexOutput output; float4x4 modelMatrix = _Object2World; float4x4 modelMatrixInverse = _World2Object; // multiplication with unity_Scale.w is unnecessary // because we normalize transformed vectors float3 normalDirection = normalize(float3( mul(float4(input.normal, 0.0), modelMatrixInverse))); float3 viewDirection = normalize(float3( float4(_WorldSpaceCameraPos, 1.0) - mul(modelMatrix, input.vertex))); float3 lightDirection; float attenuation; if (0.0 == _WorldSpaceLightPos0.w) // directional light? { attenuation = 1.0; // no attenuation lightDirection = normalize(float3(_WorldSpaceLightPos0)); } else // point or spot light { float3 vertexToLightSource = float3(_WorldSpaceLightPos0 - mul(modelMatrix, input.vertex)); float distance = length(vertexToLightSource); attenuation = 1.0 / distance; // linear attenuation lightDirection = normalize(vertexToLightSource); } float3 ambientLighting = float3(UNITY_LIGHTMODEL_AMBIENT) * float3(_Color); float3 diffuseReflection = attenuation * float3(_LightColor0) * float3(_Color) * max(0.0, dot(normalDirection, lightDirection)); float3 specularReflection; if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side? { specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection } else // light source on the right side { specularReflection = attenuation * float3(_LightColor0) * float3(_SpecColor) * pow(max(0.0, dot( reflect(-lightDirection, normalDirection), viewDirection)), _Shininess); } output.col = float4(ambientLighting + diffuseReflection + specularReflection, 1.0); output.pos = mul(UNITY_MATRIX_MVP, input.vertex); return output; } float4 frag(vertexOutput input) : COLOR { return input.col; } ENDCG } Pass { Tags { "LightMode" = "ForwardAdd" } // pass for additional light sources Blend One One // additive blending Cull Back // render only front faces CGPROGRAM #pragma vertex vert #pragma fragment frag // User-specified properties uniform float4 _Color; uniform float4 _SpecColor; uniform float _Shininess; uniform float4 _BackColor; uniform float4 _BackSpecColor; uniform float _BackShininess; // The following built-in uniforms (apart from _LightColor0) // are defined in "UnityCG.cginc", which could be #included uniform float4 unity_Scale; // w = 1/scale; see _World2Object uniform float3 _WorldSpaceCameraPos; uniform float4x4 _Object2World; // model matrix uniform float4x4 _World2Object; // inverse model matrix // (all but the bottom-right element have to be scaled // with unity_Scale.w if scaling is important) uniform float4 _WorldSpaceLightPos0; // position or direction of light source uniform float4 _LightColor0; // color of light source (from "Lighting.cginc") struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; }; struct vertexOutput { float4 pos : SV_POSITION; float4 col : COLOR; }; vertexOutput vert(vertexInput input) { vertexOutput output; float4x4 modelMatrix = _Object2World; float4x4 modelMatrixInverse = _World2Object; // multiplication with unity_Scale.w is unnecessary // because we normalize transformed vectors float3 normalDirection = normalize(float3( mul(float4(input.normal, 0.0), modelMatrixInverse))); float3 viewDirection = normalize(float3( float4(_WorldSpaceCameraPos, 1.0) - mul(modelMatrix, input.vertex))); float3 lightDirection; float attenuation; if (0.0 == _WorldSpaceLightPos0.w) // directional light? { attenuation = 1.0; // no attenuation lightDirection = normalize(float3(_WorldSpaceLightPos0)); } else // point or spot light { float3 vertexToLightSource = float3(_WorldSpaceLightPos0 - mul(modelMatrix, input.vertex)); float distance = length(vertexToLightSource); attenuation = 1.0 / distance; // linear attenuation lightDirection = normalize(vertexToLightSource); } float3 diffuseReflection = attenuation * float3(_LightColor0) * float3(_Color) * max(0.0, dot(normalDirection, lightDirection)); float3 specularReflection; if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side? { specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection } else // light source on the right side { specularReflection = attenuation * float3(_LightColor0) * float3(_SpecColor) * pow(max(0.0, dot( reflect(-lightDirection, normalDirection), viewDirection)), _Shininess); } output.col = float4(diffuseReflection + specularReflection, 1.0); // no ambient contribution in this pass output.pos = mul(UNITY_MATRIX_MVP, input.vertex); return output; } float4 frag(vertexOutput input) : COLOR { return input.col; } ENDCG } Pass { Tags { "LightMode" = "ForwardBase" } // pass for ambient light and first light source Cull Front// render only back faces CGPROGRAM #pragma vertex vert #pragma fragment frag // User-specified properties uniform float4 _Color; uniform float4 _SpecColor; uniform float _Shininess; uniform float4 _BackColor; uniform float4 _BackSpecColor; uniform float _BackShininess; // The following built-in uniforms (apart from _LightColor0) // are defined in "UnityCG.cginc", which could be #included uniform float4 unity_Scale; // w = 1/scale; see _World2Object uniform float3 _WorldSpaceCameraPos; uniform float4x4 _Object2World; // model matrix uniform float4x4 _World2Object; // inverse model matrix // (all but the bottom-right element have to be scaled // with unity_Scale.w if scaling is important) uniform float4 _WorldSpaceLightPos0; // position or direction of light source uniform float4 _LightColor0; // color of light source (from "Lighting.cginc") struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; }; struct vertexOutput { float4 pos : SV_POSITION; float4 col : COLOR; }; vertexOutput vert(vertexInput input) { vertexOutput output; float4x4 modelMatrix = _Object2World; float4x4 modelMatrixInverse = _World2Object; // multiplication with unity_Scale.w is unnecessary // because we normalize transformed vectors float3 normalDirection = normalize(float3( mul(float4(-input.normal, 0.0), modelMatrixInverse))); // negate input.normal for the back faces float3 viewDirection = normalize(float3( float4(_WorldSpaceCameraPos, 1.0) - mul(modelMatrix, input.vertex))); float3 lightDirection; float attenuation; if (0.0 == _WorldSpaceLightPos0.w) // directional light? { attenuation = 1.0; // no attenuation lightDirection = normalize(float3(_WorldSpaceLightPos0)); } else // point or spot light { float3 vertexToLightSource = float3(_WorldSpaceLightPos0 - mul(modelMatrix, input.vertex)); float distance = length(vertexToLightSource); attenuation = 1.0 / distance; // linear attenuation lightDirection = normalize(vertexToLightSource); } float3 ambientLighting = float3(UNITY_LIGHTMODEL_AMBIENT) * float3(_BackColor); float3 diffuseReflection = attenuation * float3(_LightColor0) * float3(_BackColor) * max(0.0, dot(normalDirection, lightDirection)); float3 specularReflection; if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side? { specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection } else // light source on the right side { specularReflection = attenuation * float3(_LightColor0) * float3(_BackSpecColor) * pow(max(0.0, dot( reflect(-lightDirection, normalDirection), viewDirection)), _BackShininess); } output.col = float4(ambientLighting + diffuseReflection + specularReflection, 1.0); output.pos = mul(UNITY_MATRIX_MVP, input.vertex); return output; } float4 frag(vertexOutput input) : COLOR { return input.col; } ENDCG } Pass { Tags { "LightMode" = "ForwardAdd" } // pass for additional light sources Blend One One // additive blending Cull Front // render only back faces CGPROGRAM #pragma vertex vert #pragma fragment frag // User-specified properties uniform float4 _Color; uniform float4 _SpecColor; uniform float _Shininess; uniform float4 _BackColor; uniform float4 _BackSpecColor; uniform float _BackShininess; // The following built-in uniforms (apart from _LightColor0) // are defined in "UnityCG.cginc", which could be #included uniform float4 unity_Scale; // w = 1/scale; see _World2Object uniform float3 _WorldSpaceCameraPos; uniform float4x4 _Object2World; // model matrix uniform float4x4 _World2Object; // inverse model matrix // (all but the bottom-right element have to be scaled // with unity_Scale.w if scaling is important) uniform float4 _WorldSpaceLightPos0; // position or direction of light source uniform float4 _LightColor0; // color of light source (from "Lighting.cginc") struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; }; struct vertexOutput { float4 pos : SV_POSITION; float4 col : COLOR; }; vertexOutput vert(vertexInput input) { vertexOutput output; float4x4 modelMatrix = _Object2World; float4x4 modelMatrixInverse = _World2Object; // multiplication with unity_Scale.w is unnecessary // because we normalize transformed vectors float3 normalDirection = normalize(float3( mul(float4(-input.normal, 0.0), modelMatrixInverse))); // negate input.normal for the back faces float3 viewDirection = normalize(float3( float4(_WorldSpaceCameraPos, 1.0) - mul(modelMatrix, input.vertex))); float3 lightDirection; float attenuation; if (0.0 == _WorldSpaceLightPos0.w) // directional light? { attenuation = 1.0; // no attenuation lightDirection = normalize(float3(_WorldSpaceLightPos0)); } else // point or spot light { float3 vertexToLightSource = float3(_WorldSpaceLightPos0 - mul(modelMatrix, input.vertex)); float distance = length(vertexToLightSource); attenuation = 1.0 / distance; // linear attenuation lightDirection = normalize(vertexToLightSource); } float3 diffuseReflection = attenuation * float3(_LightColor0) * float3(_BackColor) * max(0.0, dot(normalDirection, lightDirection)); float3 specularReflection; if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side? { specularReflection = float3(0.0, 0.0, 0.0); // no specular reflection } else // light source on the right side { specularReflection = attenuation * float3(_LightColor0) * float3(_BackSpecColor) * pow(max(0.0, dot( reflect(-lightDirection, normalDirection), viewDirection)), _BackShininess); } output.col = float4(diffuseReflection + specularReflection, 1.0); // no ambient contribution in this pass output.pos = mul(UNITY_MATRIX_MVP, input.vertex); return output; } float4 frag(vertexOutput input) : COLOR { return input.col; } ENDCG } } // The definition of a fallback shader should be commented out // during development: // Fallback "Specular" }
This code consists of four passes, where the first pair of passes render the front faces, and the second pair of passes renders the back faces using the negated normal vector and the back material properties. The second pass of each pair is the same as the first apart from the additive blending and the missing ambient color.
Summary
Congratulations, you made it to the end of this short tutorial with a long shader. We have seen:
- How to use front-face culling and back-face culling to apply different shaders on the two sides of a mesh.
- How to change the Phong lighting computation for back-facing triangles.
Further Reading
If you still want to know more
- about the shader version for single-sided surfaces, you should read Section “Specular Highlights”.
- about front-facing and back-facing triangles in Cg, you should read Section “Cutaways”.