Games202作业1 Unity(更新完毕)

介绍

作业1在Unity上的复现, 实现了基于的硬阴影, PCF, PCSS
代码下载: https://github.com/Eagle104fred/Games202_Homework1
Games101 阴影课程笔记:https://smuwm007.feishu.cn/docs/doccnQopokLgMTVbIPsTSxow6He
在这里插入图片描述

注意事项

  • 阴影的shader必须写在阴影的接受物体上例如墙壁等地方, 和模型本身的shader没啥关系
  • 注意深度像机的参数设置

实现过程

1.构建shadowMap以及硬阴影

  • 新建unity场景后建立一个Camera, 也可以在下面创建一个光源给模型打光不过不影响阴影效果。
  • 新建一个产生阴影的Object和一个接受阴影的Object
  • 在这里插入图片描述

在这里插入图片描述

2.设置阴影相机参数

需要增加三个文件, 一个是相机的脚本文件用于传递相机的参数和ShadowMap给Shader, 一个是ShadowMap需要新建一个RenderTexture文件来存储, 一个是用于绘制阴影的空Shader
在这里插入图片描述
LightCam.cs

public class LightCam : MonoBehaviour
{
    // Start is called before the first frame update
    public Shader shader;
    Camera mCamera;
    
    
    private void Awake()
    {
        mCamera = this.GetComponent<Camera>();
        mCamera.SetReplacementShader(shader, "");//使用shader进行渲染
        Shader.SetGlobalTexture("_ShadowMap", mCamera.targetTexture);//拿到shadowMap, 设置为全局供shader使用
   
        
    }

    // Update is called once per frame
    void Update()
    {
        Shader.SetGlobalMatrix("_ShadowLauncherMatrix", transform.worldToLocalMatrix);//保存将世界坐标转换到光源坐标的矩阵
        Shader.SetGlobalVector("_ShadowLauncherParam", new Vector4(mCamera.orthographicSize, mCamera.nearClipPlane, mCamera.farClipPlane));//存储相机内参
    }
}

ShaderDepth.shader

Shader "Custom/ShaderDepth"
{
   SubShader
	{
		Tags { "RenderType"="Opaque" }
		Offset 1,1 //绘制深度时候偏移一点位置
		Pass
		{
		}
	}
}

3.配置接受物体的shader

在这里插入图片描述

ShaderRecieve.shader

Shader "Custom/ShadowRecieve"
{

    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _BaseColor ("BaseColor", Color) = (1,1,1,1)
    }
    SubShader
    {
    Tags { "RenderType"="Opaque" }
        Pass
        {
        CGPROGRAM
        #pragma vertex vert
		#pragma fragment frag
		// make fog work
		#pragma multi_compile_fog
		#pragma enable_d3d11_debug_symbols

		#include "UnityCG.cginc"

        struct appdata{
            float4 vertex : POSITION;
            float2 shadowUV : TEXCOORD0;

        };

        struct v2f
        {
            float2 uv:TEXCOORD0;
            UNITY_FOG_COORDS(1)
            float4 vertex : SV_POSITION;
            float4 shadowPos:TEXCOORD1;
        };

        sampler2D _MainTex;
        float4 _MainTex_ST;
        sampler2D _ShadowMap;
        float4x4 _ShadowLauncherMatrix;
        float3 _ShadowLauncherParam;
        float4 _BaseColor;

        //基于shadowmap的硬阴影
        float HardShadow(v2f i)
        {
            float4 shadow = tex2Dproj(_ShadowMap,i.shadowPos);//拿到坐标在光源场景下的深度
            float shadowAlpha = shadow.r;//拿到深度值
            float2 clipalpha = saturate((0.5-abs(i.shadowPos.xy - 0.5))*20);//限定在0-1之间
            shadowAlpha *= clipalpha.x * clipalpha.y;

            float depth = 1-UNITY_SAMPLE_DEPTH(shadow);
            shadowAlpha*=step(depth,i.shadowPos.z);//如果depth<shadowPos就没有被遮挡
            return shadowAlpha;
        }
        
        v2f vert(appdata v)
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);//MVP矩阵
            float4 worldPos = mul(unity_ObjectToWorld,v.vertex);//模型空间转换到世界空间,相当于进行了model矩阵
            float4 shadowPos = mul(_ShadowLauncherMatrix,worldPos);//从世界坐标到光源坐标
            shadowPos.xy = (shadowPos.xy/_ShadowLauncherParam.x+1)/2;//再将-1,1范围转换到0,1范围用于读取shadowMap中的深度
            shadowPos.z = (shadowPos.z / shadowPos.w - _ShadowLauncherParam.y)  / (_ShadowLauncherParam.z - _ShadowLauncherParam.y);//初始化深度
            

            o.shadowPos = shadowPos;
            o.uv = TRANSFORM_TEX(v.shadowUV, _MainTex);//读取uv
			//UNITY_TRANSFER_FOG(o,o.vertex);
            return o;
        }      
        float4 frag(v2f i):SV_Target
        {
            float4 color = tex2D(_MainTex,i.uv);//拿到主颜色
            float shadowAlpha=0.0;
            shadowAlpha = HardShadow(i);  
            color.rgb *=(1-shadowAlpha)*_BaseColor.rgb;//阴影能见度加上材质本身的颜色

            return color;
        }
        ENDCG
        }
    }
}


PCF

泊松盘采样原理
PCF的本质就是每个点的阴影值都取决于附近区域的shadowMap进行采样后,再做卷积的结果,但是如果对周围区域每一个像素都算一遍shaodowMap性能开销很大,因此需要用到随机采样。

泊松盘采样

//采样点个数(泊松盘)
#define NUM_SAMPLES 150
//采样的圈数(泊松盘)
#define NUM_RINGS 10
#define pi 3.141592653589793
#define pi2 6.283185307179586

float2 poissonDisk[NUM_SAMPLES];

float rand_2to1(float2 uv )
{ 
  // 0 - 1
 const float a = 12.9898, b = 78.233, c = 43758.5453;
 float dt = dot( uv.xy, float2( a,b ) ), sn = fmod( dt, pi );
 return frac(sin(sn) * c);
}
void poissonDiskSamples(const in float2 randomSeed )
{
    float ANGLE_STEP = pi2 * float( NUM_RINGS ) / float( NUM_SAMPLES );
    float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES );
    float angle = rand_2to1( randomSeed ) * pi2;//随机初始角
    float radius = INV_NUM_SAMPLES;
    float radiusStep = radius;

    for( int i = 0; i < NUM_SAMPLES; i ++ )
    {
        poissonDisk[i] = float2( cos( angle ), sin( angle ) ) * pow( radius, 0.75 );
        radius += radiusStep;//递增半径
        angle += ANGLE_STEP;//递增角度
    }
}

PCF代码

        float PCF(v2f i)
        {
            float4 shadowCoord = i.shadowPos;
            poissonDiskSamples(shadowCoord.xy);

            float textureSize = 2048.0;//shadowMap大小
            float filterStride = 5.0;
            float filterRange = filterStride/textureSize;
            int unBlockCount = 0;
            float shadowAlpha=0.0;
            for(int i=0;i<NUM_SAMPLES;i++)
            {
                float2 sampleCoord = poissonDisk[i]*filterRange+shadowCoord.xy;//泊松偏移乘以采样间隔再加上原点位置
                
                float shadow = tex2D(_ShadowMap,sampleCoord);
                shadowAlpha = shadow.r;
                float2 clipalpha = saturate((0.5-abs(sampleCoord - 0.5))*20);//限定在0-1之间
                shadowAlpha *= clipalpha.x * clipalpha.y;//阴影区域裁剪
                float depth = 1-UNITY_SAMPLE_DEPTH(shadow);
                if(depth+0.005<shadowCoord.z)//避免自遮挡
                {
                    unBlockCount++; //计算未被遮挡的采样点数量
                }

            }
            return float(unBlockCount)/float(NUM_SAMPLES)*shadowAlpha;//阴影值为未遮挡概率
        }

PCSS

PCSS主要分为三部:
1.查找每一个像素的平均深度, 这个过程和pcf很像只不过是统计的是Block而不是unBlock。
2.根据公式就是那个该像素点的Blocker平均深度,计算软阴影的大小。 w P e n u m b r a = ( d R e c e i v e r − d B l o c k e r ) ∗ w L i g h t d B l o c k e r w_{Penumbra}=\frac{(d_{Receiver}-d_{Blocker})*w_{Light}}{d_{Blocker}} wPenumbra=dBlocker(dReceiverdBlocker)wLight
3.正常计算PCF。
findBlocker找到平均深度:

float _findBlocker(float4 shadowPos,float3 normal)
        {
            float4 shadowCoord = shadowPos;
            poissonDiskSamples(shadowCoord.xy);

            float textureSize = 2048.0;

            //注意 block 的步长要比 PCSS 中的 PCF 步长长一些,这样生成的软阴影会更加柔和
            float filterStride = 20.0;
            float filterRange = 1.0 / textureSize * filterStride;

            int shadowCount = 0;
            float blockDepth = 0.0;
            for(int i=0;i<NUM_SAMPLES;i++)
            {
                float2 sampleCoord = poissonDisk[i]*filterRange+shadowCoord.xy;
                float shadow = tex2D(_ShadowMap,sampleCoord);
                float depth = 1-UNITY_SAMPLE_DEPTH(shadow);

                //计算动态bias(根据光源和法线的夹角动态调整bias)
                float bias=min(0.009*(abs(1-dot(normal,_ShadowLightDirection))),0.0005);//将dot的0-1取值归一化到0-0.009
                if(depth+bias<shadowCoord.z)
                {
                    blockDepth+=depth;
                    shadowCount++; //计算未被遮挡的采样点数量
                }
            }
            if(shadowCount==NUM_SAMPLES)return 3.0;
            return blockDepth/float(shadowCount);
            
        }

PCSS:

float PCSS(v2f i)
        {
            float4 shadowCoord = i.shadowPos;
            // STEP 1: avgblocker depth    
            
            float3 normal = normalize(i.normal);//用于计算动态bias
            float zBlocker = _findBlocker(shadowCoord,normal);
            //if(zBlocker<EPS)return 0.0;
            //if(zBlocker>1.0)return 1.0;

            // STEP 2: penumbra size
            float W_LIGHT=1.0;
            float wPenumbra = (shadowCoord.z-zBlocker) * W_LIGHT / zBlocker;

            // STEP 3: PCF
            float textureSize = 1024.0;
            float filterStride = 10.0;
            float filterRange = filterStride/textureSize*wPenumbra;//用距离光源的距离控制软阴影大小
            int unBlockCount = 0;
            float shadowAlpha=0.0;

            
            for(int i=0;i<NUM_SAMPLES;i++)
            {
                float2 sampleCoord = poissonDisk[i]*filterRange+shadowCoord.xy;
                
                float shadow = tex2D(_ShadowMap,sampleCoord);
                shadowAlpha = shadow.r;
                float2 clipalpha = saturate((0.5-abs(sampleCoord - 0.5))*20);//限定在0-1之间
                shadowAlpha *= clipalpha.x * clipalpha.y;//阴影区域裁剪
                float depth = 1-UNITY_SAMPLE_DEPTH(shadow);

                //计算动态bias(根据光源和法线的夹角动态调整bias)
                float bias=max(0.01*(1-abs(dot(normal,_ShadowLightDirection))),0.005);//将dot的0-1取值归一化到0-0.009
                if(depth+bias<shadowCoord.z)
                {
                    unBlockCount++; //计算未被遮挡的采样点数量
                }

            }
            return float(unBlockCount)/float(NUM_SAMPLES)*shadowAlpha;
        }
  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
您好!对于Unity中的音频播放完毕的问题,您可以通过以下几种方式来处理: 1. 使用Unity的AudioSource组件:在音频播放完毕时,Unity的AudioSource组件会自动触发一个事件。您可以通过添加一个事件侦听器来监听这个事件,并在事件被触发时执行相应的操作。例如,您可以在事件触发时播放下一个音频或执行其他逻辑。 ```csharp using UnityEngine; public class AudioPlayer : MonoBehaviour { private AudioSource audioSource; private void Start() { audioSource = GetComponent<AudioSource>(); audioSource.loop = false; audioSource.playOnAwake = false; audioSource.clip = /* 设置您要播放的音频 */; audioSource.Play(); } private void Update() { if (!audioSource.isPlaying) { // 音频播放完毕的逻辑处理 // 播放下一个音频或执行其他操作 } } } ``` 2. 使用Unity的事件系统:您可以自定义一个音频播放完毕的事件,并在音频播放完毕时手动触发该事件。其他脚本可以通过添加事件侦听器来监听这个事件,并在事件被触发时执行相应的操作。 ```csharp using UnityEngine; using UnityEngine.Events; public class AudioPlayer : MonoBehaviour { public UnityEvent onAudioFinished; private AudioSource audioSource; private void Start() { audioSource = GetComponent<AudioSource>(); audioSource.loop = false; audioSource.playOnAwake = false; audioSource.clip = /* 设置您要播放的音频 */; audioSource.Play(); } private void Update() { if (!audioSource.isPlaying) { // 音频播放完毕的逻辑处理 onAudioFinished.Invoke(); // 手动触发事件 } } } ``` 希望以上方法能对您有所帮助!如果有任何疑问,请随时提出。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值