首页 > 其他分享 >【Unity3D】阴影原理及应用

【Unity3D】阴影原理及应用

时间:2023-06-18 20:34:10浏览次数:53  
标签:Unity3D 映射 纹理 v2f Pass 空间 原理 阴影

1 阴影原理

​ 光源照射到不透明物体上,会向该物体的后面投射阴影,如果阴影区域存在其他物体,这些物体不被光源照射的部分就需要渲染阴影。因此,我们可以将阴影的生成抽象出 2 个流程:物体投射阴影、物体接收阴影。

img

1.1 阴影相关开关

1)开启 Light 组件渲染阴影

img

  • No Shadows:不渲染阴影
  • Hard Shadows:硬阴影(阴影边缘较清晰)
  • Soft Shadows:软阴影(阴影边缘较模糊)

2)开启投射阴影 / 接收阴影

img

  • Cast Shadows:投射阴影,取值有 Off(关闭投射)、On(开启投射)、Two Sided(双面都可以投射阴影)、Shadows Only(只投射阴影,但物体不可见)
  • Receive Shadows:接收阴影

​ 注意:投射阴影和接收阴影开关可以独立控制,不相互依赖,因此,一个物体可以只投射阴影而不接收阴影,也可以只接收阴影而不投射阴影。

1.2 物体投射阴影

1)阴影映射纹理

​ 物体的 MeshRenderer 组件开启了 Cast Shadows 后,Unity 会把该物体加入到光源的阴影映射纹理(Shadow Map)的计算中。阴影映射纹理是一张深度图,它记录了从光源位置能看到的顶点的位置和深度,即将相机放在光源位置渲染的深度纹理。

img

2)屏幕空间的阴影映射纹理

​ Unity 5 中,Unity 使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术(Screenspace Shadow Map)。其原理是:先生成相机的深度纹理和灯光的阴影映射纹理,对于场景中任意一点,以其深度纹理的 x、y 值和阴影映射纹理的 z 值作为该点的屏幕空间阴影映射纹理。注意:Unity 并没有在所有平台上都使用了这种技术并,需要显卡支持 MRT,有些移动平台就不支持该特性。

3)ShadowCaster Pass

​ 正常的渲染流程会调用 LightMode 标签为 ForwardBase 和 Additional 的 Pass,而深度映射纹理是一张深度图,不需要渲染顶点颜色,为节省性能,Unity 设计了 LightMode 为 ShadowCaster 的 Pass 专门用于渲染阴影纹理。如果当前 Shader 文件中没有 LightMode 为 ShadowCaster 的 Pass,就去 Fallback 指定的 Shader 中继续寻找,如果仍没有找到,就继续去 Fallback 里寻找,直到找到了 ShadowCaster Pass 或没有 Fallback,如果最后找到了,Unity 会使用该 Pass 更新光源的阴影映射纹理。

4)渲染阴影映射纹理

​ 对于阴影映射纹理的渲染,Unity 封装得较好,用户只需要在 Shader 的 Fallback 中指定 Diffuse、Specular 或 VertexLit 等,如下:

Fallback "Specular"

​ 内置的 Specular 中没有 ShadowCaster Pass,其 Fallback 为 VertexLit(Shader 见:Unity Editor 安装目录下的 Editor\Data\built-in-shaders-xxx\Shaders\DefaultResourcesExtra\Normal-VertexLit.shader 文件,built-in-shaders),VertexLit 中有 ShadowCaster Pass,如下:

// Pass to render object as a shadow caster
Pass {
	Name "ShadowCaster"
	Tags { "LightMode" = "ShadowCaster" }

	CGPROGRAM
	#pragma vertex vert
	#pragma fragment frag
	#pragma multi_compile_shadowcaster
	#include "UnityCG.cginc"

	struct v2f {
		V2F_SHADOW_CASTER;
	};

	v2f vert( appdata_base v )
	{
		v2f o;
		TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
		return o;
	}

	float4 frag( v2f i ) : SV_Target
	{
		SHADOW_CASTER_FRAGMENT(i)
	}
	ENDCG
}

1.3 物体接收阴影

​ 对于模型的任意顶点,使用该点对应的阴影灰度值(可以从屏幕空间的阴影映射纹理中获取)乘以光照纹理,得到的就是该点最终要显示的纹理。阴影灰度值的计算如下:

struct v2f {
    float4 pos : SV_POSITION; // 裁剪空间顶点坐标
    float3 normal: Normal; // 世界空间顶点法线向量
    float3 worldPos : TEXCOORD0; // 世界空间顶点坐标
	SHADOW_COORDS(1) // 声明一个用于阴影纹理采样的uv坐标, 入参是下一个可用的插值寄存器的索引(此处下一个可用插值寄存器是TEXCOORD1, 其索引为1)
};

v2f vert(a2v v) {
	v2f o;
	...
	TRANSFER_SHADOW(o); // 计算阴影纹理uv坐标
	return o;
}

fixed4 frag(v2f i) : SV_Target {
	...
	// fixed shadow = SHADOW_ATTENUATION(i); // 计算阴影灰度
	// return fixed4(ambient + (diffuse + specular) * shadow, 1);
	UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); // 计算衰减因子和阴影灰度的乘积
	return fixed4(ambient + (diffuse + specular) * atten, 1);
}

​ 说明:上述宏的定义在 AutoLight.cginc 文件中;这些宏中使用了上下文中部分变量进行相关计算(如:v2f 中的 pos),因此 pos 的名称不能改变。

2 阴影应用

​ Shadow.Shader

Shader "MyShader/Shadow" {
	Properties {
		_ModelColor ("Model Color", Color) = (1, 1, 1, 1) // 模型颜色
		_Specular ("Specular Color", Color) = (1, 1, 1, 1) // 镜面反射颜色
		_Gloss ("Gloss", Range(8.0, 256)) = 20 // 镜面反射光泽度
	}
 
	SubShader {
		Tags { "RenderType"="Opaque" }

		Pass { 
			Tags { "LightMode"="ForwardBase" }
		
			CGPROGRAM
			#pragma multi_compile_fwdbase
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			fixed4 _ModelColor; // 模型颜色
			fixed4 _Specular; // 镜面反射颜色
			float _Gloss; // 镜面反射光泽度
			
			struct a2v {
				float4 vertex : POSITION; // 模型空间顶点坐标
				half3 normal: NORMAL; // 模型空间顶点法线向量
			};
			
			struct v2f {
				float4 pos : SV_POSITION; // 裁剪空间顶点坐标
				float3 normal : Normal; // 世界空间顶点法线向量
                float3 worldPos : TEXCOORD0; // 世界空间顶点坐标
				SHADOW_COORDS(1)
			};
 
			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
				o.normal = UnityObjectToWorldNormal(v.normal); // 将模型空间法线向量变换到世界空间
				o.worldPos = mul(unity_ObjectToWorld, v.vertex); // 将模型空间顶点坐标变换到世界空间
				TRANSFER_SHADOW(o);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target {
				fixed3 normal = normalize(i.normal); // 世界空间法线向量
				fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 世界空间灯光向量
				fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); // 世界空间观察向量
				fixed3 halfDir = normalize(lightDir + viewDir); // 半向量
				fixed3 albedo = _ModelColor; // 模型自身颜色
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT * albedo; // 环境光
				fixed3 diffuse = _LightColor0 * albedo * max(0, dot(normal, lightDir)); // 漫反射光
				fixed3 specular = _LightColor0 * _Specular * pow(max(0, dot(normal, halfDir)), _Gloss); // 镜面反射光(Blinn Phong光照模型)
				// fixed shadow = SHADOW_ATTENUATION(i); // 计算阴影灰度
				// return fixed4(ambient + (diffuse + specular) * shadow, 1);
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); // 计算衰减因子和阴影灰度的乘积
				return fixed4(ambient + (diffuse + specular) * atten, 1);
			}
			
			ENDCG
		}
	}
 
	FallBack "Specular"
}

​ 运行效果:

img

3 帧调试器查看阴影绘制过程

​ 通过 Window → Analysis → Frame Debug 打开帧调试器,如下:

img

​ 渲染对象主要有:Camera DepthTexture(相机深度纹理)、ShadowMap(阴影映射纹理)、Screenspace ShadowMap(屏幕空间阴影映射纹理)、TempBuffer(帧缓冲区)。

1)Camera DepthTexture

img

2)ShadowMap

img

3)Screenspace ShadowMap

img

4)TempBuffer

img

​ 声明:本文转自【Unity3D】阴影原理及应用

标签:Unity3D,映射,纹理,v2f,Pass,空间,原理,阴影
From: https://www.cnblogs.com/zhyan8/p/17489694.html

相关文章

  • 【Unity3D】魔方
    1需求实现​绘制魔方中基于OpenGLES实现了魔方的绘制,实现较复杂,本文基于Unity3D实现了2~10阶魔方的整体旋转和局部旋转。​本文完整代码资源见→基于Unity3D的2~10阶魔方实现。下载资源后,进入【Build/Windows】目录,打开【魔方.exe】文件即可体验产品。......
  • k8s 深入篇———— 一些容器操作的原理[三]
    前言简单介绍一下一些容器的操作原理。正文dockerexec是怎么做到进入容器里的呢。比如说:这里有一个容器,我们可以exec进去:dockerexec-itb265/bin/sh我们为什么能看到和容器内部一样的场景呢?首先我们知道了为什么容器进程只能看到规定的namespace了,那么如果我们......
  • SpringBoot自动配置的原理
    以WebMvcAutoConfiguration自动配置的原理为例,SpringBoot内部对大量的第三方库或Spring内部库进行了默认配置,这些配置是否生效,取决于我们是否引入了对应库所需的依赖,如果有那么默认配置就会生效。如果引入springboot-starter-web那么对应的web配置就会自动配置。那么是如何自动配......
  • per-CPU变量的静态和动态分配原理
    per-CPU是2.6内核中引入的,访问per-CPU变量几乎不需要锁,每个处理器都在其自己的副本上工作。这些副本是如何生成的呢?本文尝试解答这个问题。静态per-CPU结构设计思路大体可以分为两个阶段:编译阶段和运行时阶段在编译阶段,实际上只生成了一个CPU原本。系统中所有per-CPU结构都放到了一......
  • kafka工作原理
    1.工作流程以及文件存储机制 kafka中的消息是以topic进行分类的,生产消费消息都是面向topic。 topic是逻辑上的概念,partition分区是物理上的概念,每个分区对应一个log文件,该log文件存储的就是producer生产的log数据。producer生产的数据会追加到文件末端。消费者组中的每个消费......
  • 编译原理实验二 使用lex创建词法分析器
    实验二直接上代码吧,其实我也不怎么知道哪里是致命的,课上听老师的课后,将代码写上,但是运行在环境里面还是不行,把注释去掉就可以了,不知道会不会是注释的问题%{#include<stdio.h>#include"define.h"intlineno=1; TokenTypeid2keyword(constchar*token);%}newline ......
  • 【Linux中断】中断下半部-tasklet的原理与使用
    tasklet特性(1)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行(2)多个不同的类型的tasklet可以并行在多个CPU上(3)软中断是静态分配的,在内核编译好后,就不能再改变了。但tasklet灵活很多,可以在运行时改变tasklet是在两种软中断类型的基础上实现的,因此如果不需......
  • 学习OpenAI的词向量(word embbeding)的原理
    OpenAI中的词向量(wordembedding)是基于大规模语料库的机器学习模型学习出的,通常使用神经网络来训练。这些向量被训练为能够代表词汇的语义和上下文信息,并能够在进行自然语言处理任务时被较好的应用。传统的词向量通常是将单个单词映射到一个高维空间中的向量,并根据上下......
  • 从源码级深入剖析Tomcat类加载原理
    众所周知,Java中默认的类加载器是以父子关系存在的,实现了双亲委派机制进行类的加载,在前文中,我们提到了,双亲委派机制的设计是为了保证类的唯一性,这意味着在同一个JVM中是不能加载相同类库的不同版本的类。然而与许多服务器应用程序一样,Tomcat允许容器的不同部分以及在容器上运行的......
  • 【Lock锁的使用与原理】
    (文章目录)Lock锁的使用与原理Lock锁是JavaConcurrencyAPI中的一个重要机制,它用于实现多线程并发访问共享资源时的线程同步。与synchronized关键字相比,Lock锁提供了更为灵活和强大的同步控制能力,可以实现更细粒度的锁操作,并且支持更多的特性,如可重入锁、公平锁等等。Lock锁的......