您现在的位置是:首页 >技术杂谈 >unity shader效果教程附详细代码_深度和法线纹理网站首页技术杂谈

unity shader效果教程附详细代码_深度和法线纹理

takashi_void 2025-03-02 12:01:03
简介unity shader效果教程附详细代码_深度和法线纹理

这个系列来源于书籍冯乐乐老师的《unity shader 入门精要》,
参考的代码库“https://github.com/candycat1992/Unity_Shaders_Book”
我自己跟着写了一遍代码,并在代码块里给shader代码加上了比较详细的注释
更为仔细的解释和unity shader原理和知识书里都有,本blog不做详细解释,推荐买书来看。
本项目的所有代码在https://github.com/takashiwangbh/Unity-shader-effect-reproduction/tree/main

介绍

在很多时候,除了当前屏幕的颜色信息,还需要深度和法线的信息,尤其是对于3D的物体,深度信息能决定渲染的正确与否,在使用深度纹理和法线纹理图像也不会受到纹理和光照的影响,这样的方法也会更可靠。这次使用深度和法线纹理结合运动模糊,全局雾效和边缘检测做一些测试,也是屏幕后处理效果,我们的脚本都是加在摄像机上,然后在脚本的属性中应用shader,以下给出摄像机脚本和shader代码,有时候需要给相机加一个运动脚本,这里使用的是书中的源代码,由于和内容无关这里不补充。

运动模糊

运动模糊广泛使用速度映射图来决定模糊的方向和大小,这里介绍一种利用深度纹理在片元着色器中生成速度映射图的方法,相当于使用深度纹理模拟运动模糊的场景。

这个脚本挂给相机

/*
这个脚本实现了基于深度纹理的运动模糊效果。
通过记录上一帧的视图投影矩阵,并结合当前帧的深度信息,
模拟物体运动时的模糊效果,增强画面动态表现力。
*/

using UnityEngine;
using System.Collections;

public class MotionBlurWithDepthTexture : PostEffectsBase {

    // 运动模糊的 Shader
    public Shader motionBlurShader;
    private Material motionBlurMaterial = null;

    // 延迟加载材质,确保 Shader 检查通过后创建材质
    public Material material {  
        get {
            motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
            return motionBlurMaterial;
        }  
    }

    // 当前摄像机
    private Camera myCamera;
    public Camera camera {
        get {
            if (myCamera == null) {
                myCamera = GetComponent<Camera>();
            }
            return myCamera;
        }
    }

    // 模糊强度参数
    [Range(0.0f, 1.0f)]
    public float blurSize = 0.5f;

    // 上一帧的视图投影矩阵
    private Matrix4x4 previousViewProjectionMatrix;
    
    // 在启用时初始化深度纹理模式和矩阵
    void OnEnable() {
        // 启用摄像机的深度纹理模式
        camera.depthTextureMode |= DepthTextureMode.Depth;

        // 初始化上一帧的视图投影矩阵
        previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
    }
    
    // 在屏后处理阶段渲染运动模糊效果
    void OnRenderImage(RenderTexture src, RenderTexture dest) {
        if (material != null) {
            // 设置模糊强度参数
            material.SetFloat("_BlurSize", blurSize);

            // 设置上一帧的视图投影矩阵
            material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);

            // 计算当前帧的视图投影矩阵和其逆矩阵
            Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
            Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;

            // 将矩阵传递给 Shader
            material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);

            // 更新上一帧的视图投影矩阵为当前帧
            previousViewProjectionMatrix = currentViewProjectionMatrix;

            // 使用材质处理源纹理并渲染到目标纹理
            Graphics.Blit(src, dest, material);
        } else {
            // 如果材质不可用,直接拷贝源纹理到目标纹理
            Graphics.Blit(src, dest);
        }
    }
}

shader脚本挂给相机脚本的属性上

/*
这个 Shader 实现了基于深度纹理的运动模糊效果。
通过记录当前帧和上一帧的视图投影矩阵,计算像素的屏幕速度,并在图像渲染过程中利用速度对纹理进行采样,实现运动模糊的效果。
*/

Shader "Unity Shaders Book/Chapter 13/Motion Blur With Depth Texture" {
    Properties {
        // 主纹理 (RGB)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        // 模糊强度
        _BlurSize ("Blur Size", Float) = 1.0
    }
    SubShader {
        CGINCLUDE
        
        #include "UnityCG.cginc"
        
        // 主纹理
        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        // 摄像机的深度纹理
        sampler2D _CameraDepthTexture;
        // 当前帧的视图投影逆矩阵
        float4x4 _CurrentViewProjectionInverseMatrix;
        // 上一帧的视图投影矩阵
        float4x4 _PreviousViewProjectionMatrix;
        // 模糊强度
        half _BlurSize;
        
        // 顶点着色器输出结构
        struct v2f {
            float4 pos : SV_POSITION;   // 顶点位置
            half2 uv : TEXCOORD0;       // 纹理坐标
            half2 uv_depth : TEXCOORD1; // 深度纹理坐标
        };
        
        // 顶点着色器
        v2f vert(appdata_img v) {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex); // 计算裁剪空间位置
            
            o.uv = v.texcoord;
            o.uv_depth = v.texcoord; // 用于深度纹理采样
            
            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                o.uv_depth.y = 1 - o.uv_depth.y; // 修正 UV 坐标
            #endif
                     
            return o;
        }
        
        // 片段着色器
        fixed4 frag(v2f i) : SV_Target {
            // 从深度纹理获取深度值
            float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
            // 计算视口坐标范围 [-1, 1]
            float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
            // 使用当前视图投影逆矩阵计算世界坐标
            float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
            float4 worldPos = D / D.w; // 转换为非齐次坐标
            
            // 当前视口位置
            float4 currentPos = H;
            // 使用上一帧的视图投影矩阵将世界坐标转换为上一帧的视口位置
            float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
            previousPos /= previousPos.w; // 转换为非齐次坐标
            
            // 计算像素速度
            float2 velocity = (currentPos.xy - previousPos.xy) / 2.0f;
            
            // 运动模糊采样
            float2 uv = i.uv;
            float4 c = tex2D(_MainTex, uv); // 初始颜色
            uv += velocity * _BlurSize;
            for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) {
                float4 currentColor = tex2D(_MainTex, uv); // 模糊采样
                c += currentColor;
            }
            c /= 3; // 平均采样结果
            
            return fixed4(c.rgb, 1.0); // 返回模糊后的颜色
        }
        
        ENDCG
        
        // 渲染 Pass
        Pass {      
            ZTest Always Cull Off ZWrite Off
                
            CGPROGRAM  
            #pragma vertex vert  
            #pragma fragment frag  
            ENDCG  
        }
    } 
    FallBack Off
}

效果展示请添加图片描述

全局雾效

unity有可以直接基于内置的灯光设置达到雾效的效果,但是如果使用shader做屏幕后处理的全局雾效的实现关键是根据深度纹理来重建每个像素在世界空间下的位置。

这个脚本挂在相机上

/*
这个脚本实现了基于深度纹理的雾效处理。
通过结合摄像机的深度信息和视锥体角点计算,可以在屏幕后处理中渲染出逼真的体积雾效果。
用户可以通过调节参数自定义雾的颜色、密度以及起始和结束范围。

*/

using UnityEngine;
using System.Collections;

public class FogWithDepthTexture : PostEffectsBase {

    // 雾效的 Shader
    public Shader fogShader;
    private Material fogMaterial = null;

    // 延迟加载材质,确保 Shader 检查通过后创建材质
    public Material material {  
        get {
            fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
            return fogMaterial;
        }  
    }

    // 当前摄像机
    private Camera myCamera;
    public Camera camera {
        get {
            if (myCamera == null) {
                myCamera = GetComponent<Camera>();
            }
            return myCamera;
        }
    }

    // 当前摄像机的变换组件
    private Transform myCameraTransform;
    public Transform cameraTransform {
        get {
            if (myCameraTransform == null) {
                myCameraTransform = camera.transform;
            }
            return myCameraTransform;
        }
    }

    // 雾的密度
    [Range(0.0f, 3.0f)]
    public float fogDensity = 1.0f;

    // 雾的颜色
    public Color fogColor = Color.white;

    // 雾的起始和结束距离
    public float fogStart = 0.0f;
    public float fogEnd = 2.0f;

    // 启用时启用深度纹理
    void OnEnable() {
        camera.depthTextureMode |= DepthTextureMode.Depth;
    }
    
    // 每帧渲染时的处理
    void OnRenderImage(RenderTexture src, RenderTexture dest) {
        if (material != null) {
            // 计算视锥体角点
            Matrix4x4 frustumCorners = Matrix4x4.identity;

            float fov = camera.fieldOfView; // 垂直视场角
            float near = camera.nearClipPlane; // 近裁剪面距离
            float aspect = camera.aspect; // 宽高比

            float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad); // 半高
            Vector3 toRight = cameraTransform.right * halfHeight * aspect;   // 水平方向向量
            Vector3 toTop = cameraTransform.up * halfHeight;                // 垂直方向向量

            // 计算四个角点的方向向量
            Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
            float scale = topLeft.magnitude / near;
            topLeft.Normalize();
            topLeft *= scale;

            Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
            topRight.Normalize();
            topRight *= scale;

            Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
            bottomLeft.Normalize();
            bottomLeft *= scale;

            Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
            bottomRight.Normalize();
            bottomRight *= scale;

            // 设置视锥体矩阵的四个角点
            frustumCorners.SetRow(0, bottomLeft);
            frustumCorners.SetRow(1, bottomRight);
            frustumCorners.SetRow(2, topRight);
            frustumCorners.SetRow(3, topLeft);

            // 将视锥体矩阵传递给 Shader
            material.SetMatrix("_FrustumCornersRay", frustumCorners);

            // 设置雾的参数
            material.SetFloat("_FogDensity", fogDensity);
            material.SetColor("_FogColor", fogColor);
            material.SetFloat("_FogStart", fogStart);
            material.SetFloat("_FogEnd", fogEnd);

            // 使用材质处理源纹理并渲染到目标纹理
            Graphics.Blit(src, dest, material);
        } else {
            // 如果材质不可用,直接拷贝源纹理到目标纹理
            Graphics.Blit(src, dest);
        }
    }
}

shader脚本挂在相机脚本的属性上

/*
这个 Shader 实现了基于深度纹理的屏幕后处理雾效。
通过结合视锥体角点和深度纹理信息,计算出每个像素的世界坐标,
并根据用户设置的雾参数(颜色、密度、范围)生成真实感的雾效。
*/

Shader "Unity Shaders Book/Chapter 13/Fog With Depth Texture" {
    Properties {
        // 主纹理 (RGB)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        // 雾的密度
        _FogDensity ("Fog Density", Float) = 1.0
        // 雾的颜色
        _FogColor ("Fog Color", Color) = (1, 1, 1, 1)
        // 雾的起始距离
        _FogStart ("Fog Start", Float) = 0.0
        // 雾的结束距离
        _FogEnd ("Fog End", Float) = 1.0
    }
    SubShader {
        CGINCLUDE
        
        #include "UnityCG.cginc"
        
        // 定义需要的变量
        float4x4 _FrustumCornersRay;   // 视锥体角点矩阵
        sampler2D _MainTex;            // 主纹理
        half4 _MainTex_TexelSize;      // 主纹理的像素大小
        sampler2D _CameraDepthTexture; // 深度纹理
        half _FogDensity;              // 雾的密度
        fixed4 _FogColor;              // 雾的颜色
        float _FogStart;               // 雾的起始距离
        float _FogEnd;                 // 雾的结束距离
        
        // 顶点着色器的输出结构
        struct v2f {
            float4 pos : SV_POSITION;       // 裁剪空间位置
            half2 uv : TEXCOORD0;           // 主纹理 UV 坐标
            half2 uv_depth : TEXCOORD1;     // 深度纹理 UV 坐标
            float4 interpolatedRay : TEXCOORD2; // 插值后的视锥射线方向
        };
        
        // 顶点着色器
        v2f vert(appdata_img v) {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex); // 计算裁剪空间位置
            
            o.uv = v.texcoord;          // 主纹理 UV 坐标
            o.uv_depth = v.texcoord;    // 深度纹理 UV 坐标
            
            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                o.uv_depth.y = 1 - o.uv_depth.y; // 修正 UV 坐标方向
            #endif
            
            // 根据纹理坐标确定视锥体的角点索引
            int index = 0;
            if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) {
                index = 0;
            } else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) {
                index = 1;
            } else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) {
                index = 2;
            } else {
                index = 3;
            }

            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                index = 3 - index; // 修正角点索引
            #endif
            
            o.interpolatedRay = _FrustumCornersRay[index]; // 设置插值后的视锥射线方向
                     
            return o;
        }
        
        // 片段着色器
        fixed4 frag(v2f i) : SV_Target {
            // 获取线性深度
            float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
            // 计算世界坐标
            float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
                        
            // 根据雾的起始和结束范围计算雾密度
            float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); 
            fogDensity = saturate(fogDensity * _FogDensity);
            
            // 获取原始颜色并混合雾的颜色
            fixed4 finalColor = tex2D(_MainTex, i.uv);
            finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
            
            return finalColor; // 返回最终颜色
        }
        
        ENDCG
        
        // 渲染 Pass
        Pass {
            ZTest Always Cull Off ZWrite Off
                
            CGPROGRAM  
            #pragma vertex vert  
            #pragma fragment frag  
            ENDCG  
        }
    } 
    FallBack Off
}

效果展示

请添加图片描述

边缘检测

尽管之前使用屏幕后处理效果得到了一种边缘检测的效果,大梅沙那种直接利用颜色信息进行边缘检测的方法会产生很多不希望得到的边缘线,如果使用深度和法线纹理上进行边缘检测,这些图像不会受到纹理和光照的影响,这种方式得到的边缘检测效果更加的可靠。

脚本挂在相机上

/*
这个脚本实现了基于法线和深度的边缘检测效果。
通过获取场景的法线和深度纹理,检测像素间的变化,
从而提取边缘信息。可以调整边缘强度、检测灵敏度和采样距离来控制最终的效果。
*/

using UnityEngine;
using System.Collections;

public class EdgeDetectNormalsAndDepth : PostEffectsBase {

    // 边缘检测的 Shader
    public Shader edgeDetectShader;
    private Material edgeDetectMaterial = null;

    // 延迟加载材质,确保 Shader 检查通过后创建材质
    public Material material {  
        get {
            edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
            return edgeDetectMaterial;
        }  
    }

    // 边缘显示强度,0 表示仅显示边缘,1 表示保留原始场景
    [Range(0.0f, 1.0f)]
    public float edgesOnly = 0.0f;

    // 边缘颜色
    public Color edgeColor = Color.black;

    // 背景颜色(非边缘区域)
    public Color backgroundColor = Color.white;

    // 采样距离,用于计算像素差异
    public float sampleDistance = 1.0f;

    // 深度检测灵敏度
    public float sensitivityDepth = 1.0f;

    // 法线检测灵敏度
    public float sensitivityNormals = 1.0f;
    
    // 启用时启用深度法线纹理
    void OnEnable() {
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
    }

    // 在后处理阶段渲染图像
    [ImageEffectOpaque]
    void OnRenderImage(RenderTexture src, RenderTexture dest) {
        if (material != null) {
            // 设置 Shader 参数
            material.SetFloat("_EdgeOnly", edgesOnly);
            material.SetColor("_EdgeColor", edgeColor);
            material.SetColor("_BackgroundColor", backgroundColor);
            material.SetFloat("_SampleDistance", sampleDistance);
            material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));

            // 使用材质处理源纹理并渲染到目标纹理
            Graphics.Blit(src, dest, material);
        } else {
            // 如果材质不可用,直接拷贝源纹理到目标纹理
            Graphics.Blit(src, dest);
        }
    }
}

shader脚本挂在相机脚本的属性里

/*
这个 Shader 实现了基于法线和深度的边缘检测效果,结合 Robert Cross 算法对纹理的深度和法线进行比较,从而提取边缘信息。
*/

Shader "Unity Shaders Book/Chapter 13/Edge Detection Normals And Depth" {
    Properties {
        // 主纹理
        _MainTex ("Base (RGB)", 2D) = "white" {}
        // 边缘显示强度
        _EdgeOnly ("Edge Only", Float) = 1.0
        // 边缘颜色
        _EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
        // 背景颜色
        _BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
        // 采样距离
        _SampleDistance ("Sample Distance", Float) = 1.0
        // 灵敏度参数
        _Sensitivity ("Sensitivity", Vector) = (1, 1, 1, 1)
    }
    SubShader {
        CGINCLUDE
        
        #include "UnityCG.cginc"
        
        // 变量定义
        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        fixed _EdgeOnly;
        fixed4 _EdgeColor;
        fixed4 _BackgroundColor;
        float _SampleDistance;
        half4 _Sensitivity;
        sampler2D _CameraDepthNormalsTexture;

        // 顶点着色器输出结构
        struct v2f {
            float4 pos : SV_POSITION;
            half2 uv[5] : TEXCOORD0;
        };
        
        // 顶点着色器
        v2f vert(appdata_img v) {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex); // 计算裁剪空间位置
            
            half2 uv = v.texcoord;
            o.uv[0] = uv;

            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                uv.y = 1 - uv.y; // 修正 UV 坐标方向
            #endif
            
            // 计算采样点的 UV 偏移
            o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1, 1) * _SampleDistance;
            o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1, -1) * _SampleDistance;
            o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 1) * _SampleDistance;
            o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1, -1) * _SampleDistance;

            return o;
        }
        
        // 比较深度和法线差异,判断是否为边缘
        half CheckSame(half4 center, half4 sample) {
            half2 centerNormal = center.xy;             // 中心像素的法线
            float centerDepth = DecodeFloatRG(center.zw); // 中心像素的深度
            half2 sampleNormal = sample.xy;             // 相邻像素的法线
            float sampleDepth = DecodeFloatRG(sample.zw); // 相邻像素的深度
            
            // 计算法线差异
            half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
            int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
            
            // 计算深度差异
            float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
            int isSameDepth = diffDepth < 0.1 * centerDepth;

            // 返回 1 表示相似,0 表示边缘
            return isSameNormal * isSameDepth ? 1.0 : 0.0;
        }
        
        // 片段着色器
        fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target {
            // 获取相邻像素的深度和法线数据
            half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
            half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
            half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
            half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
            
            // 判断边缘
            half edge = 1.0;
            edge *= CheckSame(sample1, sample2);
            edge *= CheckSame(sample3, sample4);
            
            // 根据边缘显示强度插值颜色
            fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
            fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
            
            return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly); // 返回最终颜色
        }
        
        ENDCG
        
        // 渲染 Pass
        Pass { 
            ZTest Always Cull Off ZWrite Off
            
            CGPROGRAM      
            #pragma vertex vert  
            #pragma fragment fragRobertsCrossDepthAndNormal
            ENDCG  
        }
    } 
    FallBack Off
}

效果展示

在这里插入图片描述

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。