本文参考博客:Unity Shader-Decal贴花(SelfDecal,Alpha Blend,Mesh Decal,Projector,Deferred Decal)
一说到延迟,我首先想到的就是延迟渲染,要理解这个延迟贴花效果就得先理解延迟渲染的过程。延迟渲染大体先上就是先将场景的顶点位置、颜色、法线(都转换到世界空间)渲染到到GBuffer中(可以理解为和后处理一样的将场景渲染到一张贴图上,但是GBuffer不止一张贴图——就是很多张…分别存储了颜色,法线,位置…)。这样我们就可以只计算一张贴图的像素的光照,从而达到节省性能的目的。而延迟贴花效果就是将贴花后的场景渲染到GBuffer中参与光照计算,从而改变贴花的光照效果,思想就是这么个思想。
没想到这一篇写了这么久,哈哈,感觉所有的坑我都踩完了,太难受了,不过最后的结果相当惊艳,也学到了不少东西,开心。起初我是按照参考博客的代码搞,结果发现,完全搞不出来,就是治好了了我前一篇博客无法在Scene下正常显示的毛病,DeferredDecal的光照质感完全没粗来。
Scene下图片效果:
看着是不是相当一般…因为那篇博客省去了好多细节,但是非常棒的一件事情就是他给了官方示例的链接,那就相当Nice了,想来看看我好像从来没找过Unity的官方示例…
先上我弄出来的效果图
我的天,这质感,回头看看之前的效果那简直没法比,延迟渲染NB!!
这里有两个坑,唔…把我搞惨了!!!
First:记得把摄像机的RenderingPath改成Deferred,不然你毛都看不到。
Second:记得检查摄像机的CommandBuffer是否生成成功,不然你依旧毛都看不到。(因为之前我用的是StreamVR的遗留环境,导致CommandBuffer生成失败,调了好久,可太惨了)
我这里生成了一个叫做“MyDecal”的CommandBuffer。好了上代码
我们一开始就建一个空的GameObject,然后就给他贴上这个脚本就OK了,别忘了在Inspector给cubeMesh
以及_material赋值,cubeMesh用自带的Cube网格,_material就是你贴花的材质球。
using UnityEngine;
using UnityEngine.Rendering;
[ExecuteInEditMode]
public class DeferredDecal : MonoBehaviour {
public Mesh cubeMesh = null;
public Material _material = null;
private CommandBuffer commandBuffer = null;
private void OnEnable()
{
//创建命令缓冲区,并添加到摄像机
commandBuffer = new CommandBuffer();
commandBuffer.name = "MyDecal";
//添加要在指定位置执行的命令缓冲区。
//在摄影机的渲染中定义要附加CommandBuffer对象的位置。
//CameraEvent.BeforeLighting:在延迟渲染的光照计算之前。
Camera.main.AddCommandBuffer(CameraEvent.BeforeLighting,
commandBuffer);
}
void OnRenderObject()//当摄像机渲染完场景时调用
{
//给缓冲区清除一下
commandBuffer.Clear();
//标识CommandBuffer的RenderTexture。
//调用public void SetRenderTarget(Rendering.RenderTargetIdentifier color, Rendering.RenderTargetIdentifier depth);
//GBuffer0:延迟着色GBuffer0(通常为漫反射颜色)。
//CameraTarget:当前渲染相机的目标纹理。
commandBuffer.SetRenderTarget(BuiltinRenderTextureType.GBuffer0,
BuiltinRenderTextureType.CameraTarget);
//public void DrawMesh(Mesh mesh, Matrix4x4 matrix, Material material, int submeshIndex, int shaderPass, MaterialPropertyBlock properties);
//后面三个用了默认参数
//添加“绘制网格”命令。
commandBuffer.DrawMesh(meshFilter.sharedMesh, transform.localToWorldMatrix,
meshRenderer.sharedMaterial);
}
private void OnDisable()
{
//移除命令缓冲区。
if (Camera.main == null) return;
Camera.main.RemoveCommandBuffer(CameraEvent.BeforeLighting,
commandBuffer);
}
private void DrawGizmo(bool selected)
{
var col = new Color(0.0f, 0.7f, 1f, 1.0f);
col.a = selected ? 0.3f : 0.1f;
Gizmos.color = col;
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawCube(Vector3.zero, Vector3.one);
col.a = selected ? 0.5f : 0.2f;
Gizmos.color = col;
Gizmos.DrawWireCube(Vector3.zero, Vector3.one);
}
public void OnDrawGizmos()
{
DrawGizmo(false);
}
public void OnDrawGizmosSelected()
{
DrawGizmo(true);
}
//后面三个函数是官方示例给的代码,因为我们用的是一个GameObject所以我们可以画一个正方体让他的位置更加
//具体
}
这里有个缺点就是我的代码Scene看不到贴花效果,但官方的贴花效果在Scene场景下也可以看到,大概是因为官方用的是OnWillRenderObject()来更新GBuffer而我用的是OnRenderObject(),但是OnWillRenderObject()函数是物体必须有MeshRender组件才会调用,但我们的GameObject没有MeshRender组件。官方的做法是将要贴花的GameObject统一放到一个有MeshRender组件的物体去管理,但是我为了方便考虑就用了OnRenderObject(),这样子即使没有MeshRender组件的GameObject也能更新GBuffer。
Shader代码
Shader "NewStart/DeferredDecal01"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {
}
}
SubShader
{
Fog {
Mode Off }
Tags {
"Queue"="Transparent+100"}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 screenPos : TEXCOORD1;
float3 ray : TEXCOORD2;
float3 yDir:TEXCOORD3;
};
sampler2D _MainTex;
sampler2D_float _CameraDepthTexture;
sampler2D _CameraGBufferTexture2;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.screenPos = ComputeScreenPos(o.vertex);
o.ray =UnityObjectToViewPos(v.vertex)*float3(-1,-1,1);
o.yDir=mul((float3x3)unity_ObjectToWorld,float3(0,1,0));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//深度重建视空间坐标
float2 screenuv = i.screenPos.xy / i.screenPos.w;//透视除法
float4 depth=tex2D(_CameraDepthTexture,screenuv);
float linear01Depth=Linear01Depth(depth);
float viewDepth = linear01Depth * _ProjectionParams.z;//乘以远裁剪平面,恢复原来的值
float3 viewPos = i.ray* (viewDepth / i.ray.z);
float4 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1.0));
float3 objectPos = mul(unity_WorldToObject, worldPos);
clip(float3(0.5, 0.5, 0.5) - abs(objectPos)); //abs计算输入值的绝对值。
float3 worldNormal=tex2D(_CameraGBufferTexture2,screenuv).rgb*2.0-1;
float3 yDir=normalize(i.yDir);
float nodt=dot(yDir,worldNormal);
float offset=1-nodt;
float2 uv = objectPos.xz+0.5+offset*float2(0,objectPos.y);
fixed4 col = tex2D(_MainTex, uv);
return col;
}
ENDCG
}
}
}
这个Shader代码算是祖传代码了,需要注意的是我们,将 _CameraDepthNormalsTexture换回了_CameraDepthTexture,并且定义了一个sampler2D _CameraGBufferTexture2来获取GBuffer中的法线(因为GBuffer2:延迟着色GBuffer2(通常为法线)。),并且因为GBuffer中的法线是世界方向的所以,yDir(Y轴)不再转换到视角空间而是世界空间,剩下部分的我的上一篇博客有详细的解释,此处不再赘述。
接下来要搞得是在贴花里加入法线影响,这个不难,就是要改的地方有点多,而且效果比较微妙
混合开不开,怎么个混合法,就让这个效果变得很微妙,平坦的不带法线信息的地方还好,但是自身带法线的地方呢?
开?
不开?
个人感觉没法线的效果还比较好,见仁见智吧,反正也看实际需求。
代码方面我们需要注意的是GBuffer一般不要同时读和写,所以我们要拷贝一份用来读取,参考博客里写得很详细可以去看看,我也是看了才懂。而其因为我们要修改颜色和法线要写入两个GBuffer,所以要设置一个RenderTargetIdentifier(渲染过程中生成临时的渲染纹理)数组。
using UnityEngine;
using UnityEngine.Rendering;
[ExecuteInEditMode]
public class DeferredDecal : MonoBehaviour
{
public Mesh cubeMesh = null;
public Material _material = null;
private CommandBuffer commandBuffer = null;
private void OnEnable()
{
commandBuffer = new CommandBuffer();
commandBuffer.name = "MyDecal";
Camera.main.AddCommandBuffer(CameraEvent.BeforeLighting,
commandBuffer);
}
void OnRenderObject()
{
//创建命令缓冲区
commandBuffer.Clear();
//Shader的静态方法PropertyToID通过属性名获取属性ID
int normalCopyRTId = Shader.PropertyToID("_NormalCopy");
//添加“获取临时渲染纹理”命令。简单来说就将渲染纹理是拷贝到normalCopyRTId里
//public void GetTemporaryRT(int nameID, int width, int height),后面一大串都用了默认参数就不写了
//width,height是以像素为单位的宽度,当为-1表示“相机像素宽度”。
commandBuffer.GetTemporaryRT(normalCopyRTId, -1, -1);
//Blit类似于后处理里常用的Graphics.blit-它主要用于从一个(渲染)纹理复制到另一个纹理。
commandBuffer.Blit(BuiltinRenderTextureType.GBuffer2, normalCopyRTId);
//添加“设置活动渲染目标”命令。
//标识CommandBuffer的RenderTexture。
//调用public void SetRenderTarget(Rendering.RenderTargetIdentifier color, Rendering.RenderTargetIdentifier depth);
//GBuffer0:延迟着色GBuffer0(通常为漫反射颜色)。
//GBuffer2:延迟着色GBuffer2(通常为法线)。
//CameraTarget:当前渲染相机的目标纹理。
RenderTargetIdentifier[] mrt = new RenderTargetIdentifier[]
{
BuiltinRenderTextureType.GBuffer0, BuiltinRenderTextureType.GBuffer2 };
commandBuffer.SetRenderTarget(mrt, BuiltinRenderTextureType.CameraTarget);
//public void DrawMesh(Mesh mesh, Matrix4x4 matrix, Material material, int submeshIndex, int shaderPass, MaterialPropertyBlock properties);
//添加“绘制网格”命令。
commandBuffer.DrawMesh(cubeMesh, transform.localToWorldMatrix,
_material);
//释放掉拷贝的内存
commandBuffer.ReleaseTemporaryRT(normalCopyRTId);
}
private void OnDisable()
{
//移除要在指定位置执行的命令缓冲区。
if (Camera.main == null) return;
Camera.main.RemoveCommandBuffer(CameraEvent.BeforeLighting,
commandBuffer);
}
private void DrawGizmo(bool selected)
{
var col = new Color(0.0f, 0.7f, 1f, 1.0f);
col.a = selected ? 0.3f : 0.1f;
Gizmos.color = col;
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawCube(Vector3.zero, Vector3.one);
col.a = selected ? 0.5f : 0.2f;
Gizmos.color = col;
Gizmos.DrawWireCube(Vector3.zero, Vector3.one);
}
public void OnDrawGizmos()
{
DrawGizmo(false);
}
public void OnDrawGizmosSelected()
{
DrawGizmo(true);
}
}
Shader部分要改的也是蛮多的,因为将法线贴图中的法线转换到世界坐标下,我们需要添加上X,Z轴的转换,还要定义一个_NormalCopyRT来接收拷贝的法线数据,因为我们要同时输出法线和颜色信息所以用到了MRT(多重渲染),我们还要修改frag函数,让他输出两个值。
Shader "NewStart/DeferredDecal"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {
}
_BumpMap("Normal",2D)="bump"{
}
}
SubShader
{
Tags {
"Queue"="Transparent+100"}
Pass
{
Fog {
Mode Off } // no fog in g-buffers pass
ZWrite Off
//Blend SrcAlpha OneMinusSrcAlpha
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma exclude_renderers nomrt
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 screenPos : TEXCOORD1;
float3 ray : TEXCOORD2;
float3 xDir:TEXCOORD3;
float3 yDir:TEXCOORD4;
float3 zDir:TEXCOORD5;
};
sampler2D _MainTex;
sampler2D _BumpMap;
sampler2D_float _CameraDepthTexture;
sampler2D _NormalCopyRT;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.screenPos = ComputeScreenPos(o.vertex);
o.ray =UnityObjectToViewPos(v.vertex)*float3(-1,-1,1);
o.xDir=mul((float3x3)unity_ObjectToWorld,float3(1,0,0));
o.yDir=mul((float3x3)unity_ObjectToWorld,float3(0,1,0));
o.zDir=mul((float3x3)unity_ObjectToWorld,float3(0,0,1));
return o;
}
void frag (v2f i,out half4 outAlbedo:COLOR0,out half4 outNormal:COLOR1)
{
//深度重建视空间坐标
float2 screenuv = i.screenPos.xy / i.screenPos.w;//透视除法
float4 depth=tex2D(_CameraDepthTexture,screenuv);
float linear01Depth=Linear01Depth(depth);
float viewDepth = linear01Depth * _ProjectionParams.z;//乘以远裁剪平面,恢复原来的值
float3 viewPos = i.ray* (viewDepth / i.ray.z);
float4 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1.0));
float3 objectPos = mul(unity_WorldToObject, worldPos);
clip(float3(0.5, 0.5, 0.5) - abs(objectPos)); //abs计算输入值的绝对值。
float3 worldNormal=tex2D(_NormalCopyRT,screenuv).rgb*2.0-1;
float3 yDir=normalize(i.yDir);
float nodt=dot(yDir,worldNormal);
float offset=1-nodt;
float2 uv = objectPos.xz+0.5+offset*float2(0,objectPos.y);
outAlbedo = tex2D(_MainTex, uv);
//读取法线并转换到世界空间最后再进行度法线的逆操作就OK了。
fixed3 bump=UnpackNormal(tex2D(_BumpMap,uv));
//注意这里的顺序是XZY不是XYZ!!!!!!!!!!!
float3x3 normalMat=float3x3(i.xDir,i.zDir,i.yDir);
bump=mul(bump,normalMat);
outNormal=fixed4(bump*0.5+0.5,1.0);
}
ENDCG
}
}
}
那么这篇博客到这里就结束了…才怪,还有个坑…太惨了
此情此景我不喊了一句“卧槽!”,为什么会这样子?我也说不太清,当然是延迟渲染的锅了。但是我知道解决办法,就是在场景中再添加一个光源,把主光源照不到的地方照亮就一切OK了,当然你也可以直接把主光源设置为NoShadow就可以解决,不过这样子你的场景就没阴影了,反正看情况吧。
OK,问题都解决了,Decal篇顺利结束。感谢你的阅读,如有错误,欢迎指正。