转载请注明出处
裁剪
1、 一种比较笨拙的裁剪方式:逐像素裁剪
使用discard 可以丢弃掉一些不需要渲染的像素点,需要写在fragment方法中,根据判断像素在==模型空间坐标系==的Y轴的坐标进行裁剪,比如下面的代码,就是吧一个模型的上半部分裁剪掉:
```
float4 frag(vertexOutput input) : COLOR
{
if (input.posInObjectCoords.y > 0.0)
{
discard; // drop the fragment if y coordinate > 0
}
return float4(0.0, 1.0, 0.0, 1.0); // green
}
```
先来分析一下上面这个裁剪方法吧,是在即将要渲染这个片元的时候才进行discard,要知道我们游戏中有多少个模型,又有多少个像素点要渲染,对于硬件来说,走到这一步才忽略,从性能的角度来讲,是个很糟糕的办法,所以在我们正常的shader中,不要使用这个discard,否则带来的性能问题,绝对不是你想看到的。
虽然这个方法不太好,但是我们还是需要学习它的一些东西的;上面说的实在模型坐标系中进行裁剪,那如果我们在游戏开发中,需要世界坐标系下,或者是在相机的视口下进行裁剪怎么办呢?仅仅是模型坐标系是满足不了的,那就看下下面的世界坐标系下的裁剪吧;
如果你比较熟悉Unity中的脚本编写,你可能会尝试下面的做法,在Shader中定义一个四维的矩阵参数,值是(模型的世界坐标到模型坐标的矩阵)逆转换矩阵,然后在shader中进行计算片元在世界坐标系下的坐标转换为模型坐标系下的坐标进行计算,当你拥有了模型坐标系下的坐标,那么就很容易通过上面的方式来进行discard了。
2、裁剪面
主要是对物体的某些像素点进行忽略渲染,这里是用==Cull==关键字进行裁剪面渲染;这句话要写在CGPROGRAM 前面,因为不是CG语句,是个==Unity ShaderLab命令==
Shader中的Pass块里有这么一行代码 Cull Off,这一行在CGPROGRAM之前,因为它不是Cg,事实上,它是一个Unity ShaderLab的命令,意思是关闭任何三角面的裁剪,(也就是渲染任何三角面)。为什么要设置这个Off呢,因为Shader默认的裁剪方式是Cull Back,因为我们再游戏中,模型的内表面基本是不可见的,所以如果我们不设置Off,那就会裁剪我们看不到的那一面,也就是Back,当然,如果我们不需要渲染的话,最好关闭,==性能问题要时刻注意==。
那么,Culling是怎么工作的呢,其实三角面和顶点还是正常的处理,只是在摄像机后处理阶段,GPU定义哪些三角面的某个顶点可见,哪些不可见,基于这个测试,每个三角面被分成前面和后面,如果标记了Cull Front 那么前面就会被discard,Cull Back反之,如果都没标记,那么双面都会被渲染。
So,我们能裁剪什么呢?一个程序可能使用了多个不同的Shader来渲染模型外表面,很少渲染模型内表面的,下面的Shader有两个Pass块,第一个裁剪外表面,
其他的面也就是内表面被渲染成红色,第二个Pass块裁剪了内表面,吧外表面渲染成绿色,所以整体来说,下面的Shader吧模型的内表面渲染成红色,外表面渲染成绿色。
Shader "Cg shader with two passes using discard"
{
SubShader
{
// first pass (is executed before the second pass)
Pass
{
Cull Front // cull only front faces
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct vertexInput
{
float4 vertex :POSITION;
};
struct vertexOutput
{
float4 pos : SV_POSITION;
float4 posInObjectCoords : TEXCOORD0;
};
vertexOutput vert (vertexInput input)
{
vertexOutput output;
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
output.posInObjectCoords = input.vertex;
return output;
}
float4 frag (vertexOutput input) :COLOR
{
if (input.posInObjectCoords.y > 0.0)
{
discard; // drop the fragment if y coordinate > 0
}
return float4(1.0, 0.0, 0.0, 1.0); // red
}
ENDCG
}
// second pass (is executed after the first pass)
Pass
{
Cull Back // cull only back faces
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct vertexInput
{
float4 vertex :POSITION;
};
struct vertexOutput
{
float4 pos : SV_POSITION;
float4 posInObjectCoords : TEXCOORD0;
}
;
vertexOutput vert (vertexInput input)
{
vertexOutput output;
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
output.posInObjectCoords = input.vertex;
return output;
}
float4 frag (vertexOutput input) : COLOR
{
if (input.posInObjectCoords.y > 0.0)
{
discard; // drop the fragment if y coordinate > 0
}
return float4(0.0, 1.0, 0.0, 1.0); // green
}
ENDCG
}
}
}
}
混合渲染(Blending)
这一部分主要讲如何渲染透明物体,半透明物体,透明的物体需要能够看到后面的物体,所以它的颜色是自己的颜色和该物体后面的颜色的混合色;
- 颜色混合
可编程图形管道(programmable graphics pipeline,计算每个片元的颜色输出的shader),混合了两个颜色,一个是当前显示的颜色pixel_color,一个是fragment函数计算出来的颜色 fragment_output,将这两个颜色通过不同的公式计算出来得到最终需要渲染的颜色,也就是fragment方法输出的颜色;至于使用什么公式和函数,不需要自己手动写,只需要配置即可;公式如下:
float4 result = SrcFactor * fragment_output + DstFactor * pixel_color;
SrcFactor和DscFactor是配置的float4类型RGBA颜色值
那么怎么在Shader中定义SrcFactor和Dsctor的值呢?
Blend {SrcFactor代码} {DstFactor代码}
最普通的代码就如下面的表格配置,直接替换即可:
代码 | 代表的值(SrcFactor or DstFactor) |
---|---|
One | float4(1.0) |
Zero | float4(0.0) |
SrcColor | fragment_output |
SrcAlpha | float4(fragment_output.a) |
DstColor | pixel_color |
DstColor | float4(pixel_color.a) |
OneMinusSrcColor | float4(1.0) - fragment_output |
OneMinusSrcAlpha | float4(1.0 - fragment_output.a) |
OneMinusDstColor | float4(1.0) - pixel_color |
OneMinusDstAlpha | float4(1.0 - pixel_color.a) |
float4(1.0) 是一种简写方式,完全写法是float4(1.0,1.0,1.0,1.0)
- 透明度混合
举个栗子吧,一个比较简单的混合等式,透明度混合,在Unity 的Shader中定义如下:
Blend SrcAlpha OneMinusSrcAlpha
那上面的这句话执行了什么呢,是怎么计算的?请看下面的公式,
float4 result = float4(fragment_output.a) * fragment_output + float4(1.0 - fragment_output.a) * pixel_color;
==需要注意的点==
- Tags { "Queue" = "Transparent"}
需要加入Tags标签,标记 ,这一行的意思是让所有透明的物体,在所有不透明的物体渲染完毕后再渲染,主要也是关于深度缓冲的问题,不透明的物体渲染会被透明的物体渲染覆盖掉,为了解决这个问题,我们首先渲染完全不透明的网格,然后再渲染透明的网格,一个网格是否被渲染成透明和完全不透明,就由这个标签决定
- ZWrite Off
关闭写入深度缓冲,深度缓冲保留了了片元的深度值,丢弃了比它的深度值高的片元,对于透明渲染来讲,我们需要看透目标透明物体,因此,透明物体渲染的时候,不应该剔除之前已经渲染到该片元的颜色深度缓冲,所以UnityShaderLab引入了剔除和深度测试。
渲染的顺序其实还是会影响到渲染结果的,比如你透过一个几乎不透明的绿色玻璃杯,看后面的一个几乎不透明的红色玻璃杯,那你几乎只能看到绿色,相反过来,你几乎只能看到红色,所以渲染的的前后顺序不同,出来的渲染效果就会有所不同,为了避免这种失真,所以建议使用(Additive-Bleding)饱和相加的方法来混合源像素和目标像素的颜色分量,或者使用小阴影来解决这种失真效果。
上面的例子确实可以不渲染物体的内表面,可是一旦我们从外部看透明物体,我们还是应该渲染内部,使用Cull Off可以渲染内表面,,然而,如果我们只是禁用裁剪,我们可能得到一个问题:如上面所说,它造成透明的物体本来应该裁剪的片元也被渲染出来了,会导致三角面被渲染的顺序是乱掉的。因此,我们需要确定内表面先被渲染,然后再渲染外表面,在Unity ShaderLab中可以使用两个Pass块在不同的Order下来渲染同一个网格;也就是第一个Pass块先渲染内表面,另一个再渲染外表面
小Tip: 比如要渲染两个物体A和B,A是完全不透明物体,B是半透明物体,透过B渲染的话,需要先渲染A,然后渲染B的内表面,然后是B的外表面,所以渲染B就需要使用上面说的两个Pas块。
透明顺序排列(Order-Independent Transparency)
上一小节说到,三角面的渲染顺序决定着混合渲染的结果;这一节主要描述如果避免排序问题导致的渲染问题;
- Additive Blending (相加混合)
颜色被这种方式叠加以后,多张图片叠加起来看,会让我们很难看出来图片到底哪个在前,哪个灾后,怎么实现还是用上面讲到的那个混合公式,但是这个混合方式的 DstFactor值必须是One,SrcFactor不能依赖pixel-color,值可以使One,SrcColor,SrcAlpha,OneMinusSrcColor,OneMinusSrcAlpha;这种混合方法一半用于处理光源和爆炸效果
- Multiplicative Blending (相乘混合)
这种方式是用多个灰度过滤器,过滤器应用到相机上的顺序不会影响对图像的减弱程度,然后在三角面光栅化之前,保持帧缓冲器内的强弱度一致。
SrcFactor必须为 Zero ,DstFactor必须依靠片元颜色(fragment color),可以是SrcColor,SrcAlpha,OneMinusSrcColor,OneMinusSrcAlpha;
Shader "Cg shader using order-independent blending" {
SubShader {
Tags { "Queue" = "Transparent" }
// draw after all opaque geometry has been drawn
Pass {
Cull Off // draw front and back faces
ZWrite Off // don't write to depth buffer
// in order not to occlude other objects
Blend Zero OneMinusSrcAlpha // multiplicative blending
// for attenuation by the fragment's 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(1.0, 0.0, 0.0, 0.3);
}
ENDCG
}
Pass {
Cull Off // draw front and back faces
ZWrite Off // don't write to depth buffer
// in order not to occlude other objects
Blend SrcAlpha One // additive blending to add colors
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(1.0, 0.0, 0.0, 0.3);
}
ENDCG
}
}
}
注意:上面的两个Pass块的顺序非常重要,第一个是背景是减弱的,第二章是增强的
Silhouette Enhancement(增强轮廓)
这个主要是想实现一个效果:物体的整体的透明度是不一样的,也就是在渲染这个物体的时候,它的不同的部位的透明度是不一样的,在没有光照的情况下,也要有三维立体感,所以这一节需要使用到物体的法线向量。
- 平滑表面的轮廓
通过计算点到观察点的方向V和点的法线向量N是否垂直来判定该点是不是在这个表面上;也就是如果V点乘N之后得到的结果为0,就认为该点不在物体的表面上,如果得到的值接近于0,那么我们就认为改点在物体表面上;
Increasing the Opacity at Silhouettes (增加轮廓的不透明度)
为了达到我们想要的效果,如果某个点的观察方向和该点的法线向量的点乘结果接近于0,那么我们应该提升某个点的不透明度,也就是值越小,越不透明;具体计算公式如下:
\alpha^` = min{\left(1, {\frac{\left(X\right)}{|VN|}}\right)}
在Shader中实现公式
为了实现上面的公式,有以下几个问题:
这个计算是写在那个函数中呢?fragment还是vertex呢?
在一些情况下,很明显因为实现需要进行纹理贴图,而纹理贴图必须写在fragment中,但是如果把代码实现在vertex方法中呢,运行效率会更高(当然除了低分辨率的图片),因此,如果你更注重性能,选择在vertex中实现更好;相反,如果你更注重图片的质量,就要写在fragment中了,具体情况具体定吧,总之就在逐像素光照和逐顶点光照进行平衡;在哪个坐标系下进行公式计算?
又出现两个选择,在世界坐标系下是个好的选择,因为在Unity中很多变量的定义都是在世界坐标系下的最后一个问题是:我们从哪里获得公式的参数呢?
比如法线向量,从点到观察位置的方向怎么获取呢,alpha可以在Shader的属性中定义,法线向量是个标准的顶点输入参数,这个没问题,但是观察方向需要在shader中使用Unity定义好的_WorldSpaceCameraPos(就是世界坐标系下相机的位置)来计算得到;因此,我们必须在进行计算之前先把顶点坐标和法线向量转换到世界坐标系中;转换的矩阵 _Object2World 是将顶点坐标从模型空间下转换到世界坐标系下,还有一个它的逆矩阵 _World2Object可以从世界坐标系下转换到模型坐标系下;
//下面是观察方向的计算公式,世界坐标系下相机位置减去顶点的世界坐标系下的位置得到的向量就是观察向量
output.viewDir = normalize(_WorldSpaceCameraPos- float3(mul(modelMatrix, input.vertex)));
另外一步是法线向量就需要通过逆矩阵来从世界坐标下到模型空间下,特别注意这里是==左乘==,具体的矩阵计算,请自行学习,这里不做补充;
output.normal = normalize(float3(mul(float4(input.normal, 0.0), _World2Object)));
注意最终计算出来的透明度可能大于1,所以要进行一下min函数计算
完成代码如下
Shader "Cg silhouette enhancement" {
Properties {
_Color ("Color", Color) = (1, 1, 1, 0.5)
// user-specified RGBA color including opacity
}
SubShader
{
Tags { "Queue" = "Transparent" }
// draw after all opaque geometry has been drawn
Pass {
ZWrite Off // don't occlude other objects
Blend SrcAlpha OneMinusSrcAlpha // standard alpha blending
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
uniform float4 _Color; // define shader property for shaders
// The following built-in uniforms are also 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)
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float3 normal : TEXCOORD;
float3 viewDir : TEXCOORD1;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
// multiplication with unity_Scale.w is unnecessary
// because we normalize transformed vectors
output.normal = normalize(float3(
mul(float4(input.normal, 0.0), modelMatrixInverse)));
output.viewDir = normalize(_WorldSpaceCameraPos
- float3(mul(modelMatrix, input.vertex)));
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normal);
float3 viewDirection = normalize(input.viewDir);
float newOpacity = min(1.0, _Color.a
/ abs(dot(viewDirection, normalDirection)));
return float4(float3(_Color), newOpacity);
}
}
ENDCG
}
}
}