本教程涵盖了半透明体。
这是几个关于光照教程中的其中之一,这个光照超出了Phone反射模型的范围。但是,它是基于章节“光滑镜面高光”中描述的用Phone反射模型实现的逐像素光照。如果你没阅读过那章,建议先阅读一下。
蜡质效果
不幸的是,在实时游戏引擎中,光线在半透明体中的传输是相当大的挑战。从光源的视点渲染一张深度图可能会有用,但这已不在本章的范围。因此,我们将会伪造一些次表面的效果。
第一种效果被称为“蜡质”,它描述了蜡的光滑、有光泽的外观,缺乏漫反射能提供的硬对比。理论上,我们在计算漫反射(但不是镜面反射)之前会平滑法线向量,并且实际上使用法线贴图还是有可能的。但是,这里采用另一种方法。为了柔化漫反射的由max(0, N·L)(参考章节“漫反射”)这项造成的硬对比,我们把蜡质w从0增加到1来减少这项的影响。更具体地说,我们会用1- w去乘以max(0, N·L)这项。但是,这样不只是会减弱硬对比还会减弱整个照明的亮度。为了避免这种情况,由于次表面散射我们添加蜡质w来伪造额外的灯光,这是更强大的“蜡质”材质。
于是,我们把漫反射等式
替换成这个等式:
蜡质w介于0(也就是正规的漫反射)到1(也就是不依赖于N·L)之间。
这个方法实现起来很简单,对GPU来说容易计算,容易控制,并且它确实像蜡和玉的样子,特别是它跟有高光的镜面高光结合起来的时候。
背光的半透明
我们要伪造的第二种效果就是背光,它穿透一个物体并且在物体可见的前面出去。这种效果越强烈,背面和前面的距离就越近,也就是特别在轮廓处,背面和前面的距离实际上变成了0.因此,我们可以使用章节“轮廓增强”中讨论过的技术来轮廓处生成更多的光照。但是,如果我们把一个封闭网格的背面的实际漫射照明考虑进来的话,这个效果就会更有说服力。最后,我们这样做:
- 我们只渲染背面,然后用一个描述点(在背面)距离轮廓多远的加权因子来计算漫反射。我们用不透明度为0来标记这个像素。(通常在帧缓冲中的像素不透明度为1。通过设置它们的不透明度为0来标记像素的技术是基于在后面通道中的混合方程中的帧缓冲中使用alpha值的可能性;参考章节“透明度”。)
- 我们只渲染正面,然后把所有不透明度为1的像素的颜色设置为黑色(也就是我们在第一步中没有被光栅化的所有像素)。当另一个物体跟网格相交时,这是必要的。
- 我们用来自正面的光照再次渲染正面,并且把帧缓冲中的颜色乘以描述点(在正面)距离轮廓有多远的系数。在第一和第三步中,我们使用轮廓系数
1 - | N·L |
,它在轮廓处的值为1并且在观察者直视表面时值为0。(可以引入点积的指数来允许更多的艺术控制。)于是,所有的计算实际上就相当简单了。最复杂的部分就是混合。
实现
The implementation relies heavily on blending, which is discussed in Section “Transparency”. In addition to three passes corresponding to the steps mentioned above, we also need two more additional passes for additional light sources on the back and the front. With so many passes, it makes sense to get a clear idea of what the render passes are supposed to do. To this end, a skeleton of the shader without the Cg code is very helpful:
这个实现严重依赖于混合,它在章节“透明度”中有过讨论。除了上面提到的步骤对应的三个通道以外,我们也需要为在背面和正面的额外光源准备两个额外的通道。有了这么多通道,我们对于渲染通道应该做什么有了一个清晰的认识。最后,没有Cg代码的着色器框架是很有用的:
Shader "Cg translucent bodies" {
Properties {
_Color ("Diffuse Color", Color) = (1,1,1,1)
_Waxiness ("Waxiness", Range(0,1)) = 0
_SpecColor ("Specular Color", Color) = (1,1,1,1)
_Shininess ("Shininess", Float) = 10
_TranslucentColor ("Translucent Color", Color) = (0,0,0,1)
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" } // pass for
// ambient light and first light source on back faces
Cull Front // render back faces only
Blend One Zero // mark rasterized pixels in framebuffer
// with alpha = 0 (usually they have alpha = 1)
CGPROGRAM
[...]
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources on back faces
Cull Front // render back faces only
Blend One One // additive blending
CGPROGRAM
[...]
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardBase" } // pass for
// setting pixels that were not rasterized to black
Cull Back // render front faces only (default behavior)
Blend Zero OneMinusDstAlpha // set colors of pixels
// with alpha = 1 to black by multiplying with 1-alpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 vertexPos : POSITION) : SV_POSITION
{
return mul(UNITY_MATRIX_MVP, vertexPos);
}
float4 frag(void) : COLOR
{
return float4(0.0, 0.0, 0.0, 0.0);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardBase" } // pass for
// ambient light and first light source on front faces
Cull Back // render front faces only
Blend One SrcAlpha // multiply color in framebuffer
// with silhouetteness in fragment's alpha and add colors
CGPROGRAM
[...]
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources on front faces
Cull Back // render front faces only
Blend One One // additive blending
CGPROGRAM
[...]
ENDCG
}
}
Fallback "Specular"
}
这个框架还是相当长的;但是,它提供给我们一个完整着色器是怎么组织的好想法。
完整的着色器代码
在以下的完整着色器代码中,请注意替换_Color
的属性_TranslucentColor
被用在背面漫射和环境部分的计算中。也要注意“轮廓”是如何跟在正面一样在背面计算的。但是,它只是直接地跟背面的片段颜色相乘。在正面,它只是间接地跟片段颜色的alpha分量相乘,并且把alpha跟最终颜色(在帧缓冲中像素的颜色)进行混合。最终,这个“蜡质”只是被用在正面的漫反射中。
Shader "Cg translucent bodies" {
Properties {
_Color ("Diffuse Color", Color) = (1,1,1,1)
_Waxiness ("Waxiness", Range(0,1)) = 0
_SpecColor ("Specular Color", Color) = (1,1,1,1)
_Shininess ("Shininess", Float) = 10
_TranslucentColor ("Translucent Color", Color) = (0,0,0,1)
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" } // pass for
// ambient light and first light source on back faces
Cull Front // render back faces only
Blend One Zero // mark rasterized pixels in framebuffer
// with alpha = 0 (usually they should have alpha = 1)
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float _Waxiness;
uniform float4 _SpecColor;
uniform float _Shininess;
uniform float4 _TranslucentColor;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normalDir);
float3 viewDirection = normalize(
_WorldSpaceCameraPos - input.posWorld.xyz);
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 ambientLighting = _TranslucentColor.rgb
* UNITY_LIGHTMODEL_AMBIENT.rgb;
float3 diffuseReflection = _TranslucentColor.rgb
* attenuation * _LightColor0.rgb
* max(0.0, dot(normalDirection, lightDirection));
float silhouetteness =
1.0 - abs(dot(viewDirection, normalDirection));
return float4(silhouetteness
* (ambientLighting + diffuseReflection), 0.0);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources on back faces
Cull Front // render back faces only
Blend One One // additive blending
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float _Waxiness;
uniform float4 _SpecColor;
uniform float _Shininess;
uniform float4 _TranslucentColor;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normalDir);
float3 viewDirection = normalize(
_WorldSpaceCameraPos - input.posWorld.xyz);
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 diffuseReflection = _TranslucentColor.rgb
* attenuation * _LightColor0.rgb
* max(0.0, dot(normalDirection, lightDirection));
float silhouetteness =
1.0 - abs(dot(viewDirection, normalDirection));
return float4(silhouetteness * diffuseReflection, 0.0);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardBase" } // pass for
// setting pixels that were not rasterized to black
Cull Back // render front faces only (default behavior)
Blend Zero OneMinusDstAlpha // set colors of pixels
// with alpha = 1 to black by multiplying with 1-alpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 vertexPos : POSITION) : SV_POSITION
{
return mul(UNITY_MATRIX_MVP, vertexPos);
}
float4 frag(void) : COLOR
{
return float4(0.0, 0.0, 0.0, 0.0);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardBase" } // pass for
// ambient light and first light source on front faces
Cull Back // render front faces only
Blend One SrcAlpha // multiply color in framebuffer
// with silhouetteness in fragment's alpha and add colors
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float _Waxiness;
uniform float4 _SpecColor;
uniform float _Shininess;
uniform float4 _TranslucentColor;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normalDir);
float3 viewDirection = normalize(
_WorldSpaceCameraPos - input.posWorld.xyz);
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 ambientLighting =
UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* (_Waxiness + (1.0 - _Waxiness)
* 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 * _LightColor0.rgb
* _SpecColor.rgb * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
float silhouetteness =
1.0 - abs(dot(viewDirection, normalDirection));
return float4(ambientLighting + diffuseReflection
+ specularReflection, silhouetteness);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources on front faces
Cull Back // render front faces only
Blend One One // additive blending
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float _Waxiness;
uniform float4 _SpecColor;
uniform float _Shininess;
uniform float4 _TranslucentColor;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normalDir);
float3 viewDirection = normalize(
_WorldSpaceCameraPos - input.posWorld.xyz);
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* (_Waxiness + (1.0 - _Waxiness)
* 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 * _LightColor0.rgb
* _SpecColor.rgb * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
float silhouetteness =
1.0 - abs(dot(viewDirection, normalDirection));
return float4(diffuseReflection
+ specularReflection, silhouetteness);
}
ENDCG
}
}
Fallback "Specular"
}
总结
恭喜!你完成了关于半透明体的教程,我们学到了:
- 如何伪造蜡的外观。
- 如何伪造背光照射的半透明材质的轮廓的外观。
- 如何实现这些技术。