Metal学习笔记九:光照基础
光和阴影是使场景流行的重要要求。通过一些着色器艺术,您可以突出重要的对象、描述天气和一天中的时间并设置场景的气氛。即使您的场景由卡通对象组成,如果您没有正确地照亮它们,场景也会变得平淡无奇。
最简单的光照方法之一是 Phong 反射模型。它以 Bui Tong Phong 的名字命名,他在 1975 年发表了一篇论文,扩展了旧的光照模型。这个想法不是尝试复制光线和反射物理学,而是生成看起来逼真的图片。
这种模型已经流行了 40 多年,是开始使用几行代码来学习如何伪造光照的好地方。所有计算机图像都是假的,但有更现代的实时渲染方法可以模拟光的物理特性。
在第11章“地图和材质”中,您将了解基于物理的渲染(PBR),这是您的渲染器最终将使用的照明技术。PBR 是一种更逼真的光照模型,但 Phong 易于理解和入门。
starter项目
打开本节的starter项目。
起始项目的文件现在位于合理的分组中。在 Game 组中,项目包含一个新的游戏控制器类,该类进一步分离了场景更新和渲染。Renderer 现在独立于 GameScene。GameController 初始化并拥有 Renderer 和 GameScene。在每一帧上,作为 MetalView 的代理,GameController 首先更新场景,然后将其传递给 Renderer 进行绘制。
在 GameScene.swift 中,新场景包含一个球体和一个指示场景旋转的 3D 小工具。
Utility 组中的 DebugLights.swift 包含一些代码,您稍后将使用这些代码来调试光源的位置。点光源将绘制为点,太阳的方向将绘制为线条。
熟悉代码,构建并运行项目。
为了围绕球体旋转并充分欣赏您的光照,相机是 ArcballCamera 类型。按 1(在 Alpha 键上方)将摄像机设置为前视图,按 2 将摄像机重置为默认视图。GameScene 包含用于此功能的按键代码。
您可以看到球体颜色非常平坦。在本章中,您将添加着色光照和镜面反射高光。
颜色表示
在本书中,您将学习必要的基础知识,以渲染光线、颜色和简单的着色。然而,光的物理学是一个庞大而迷人的话题,有许多书籍和很大一部分互联网专门用于讲解它。您可以在本章 resources 目录中的 references.markdown 中找到进一步的阅读内容。
在现实世界中,不同波长的光的反射赋予了对象颜色。吸收所有光的对象表面是黑色的。在计算机世界中,像素显示颜色。像素越多,分辨率越好,这使得生成的图像更清晰。每个像素都由子像素组成。这些是预先确定的单一颜色,红色、绿色或蓝色。通过打开和关闭这些子像素,根据颜色深度,屏幕可以显示人眼可见的大部分颜色。
在 Swift 中,您可以使用该像素的 RGB 值来表示颜色。例如,float3(1, 0, 0) 是红色像素,float3(0, 0, 0) 是黑色,float3(1, 1, 1) 是白色。
从着色的角度来看,您可以通过将两个值相乘来将红色表面与灰色光照组合在一起:
let result = float3(1.0, 0.0, 0.0) * float3(0.5, 0.5, 0.5) 结果是 (0.5, 0, 0),这是一个较深的红色着色。
对于简单的 Phong 光照,我们可以使用表面的朝向。物体表面离光源越远,表面就越暗。
法线
表面的斜率可以确定表面反射光线的程度。
在下图中,点 A 正对着太阳,将接收到最多的光;点B不那么直接朝向太阳,但仍会接收到一些光线;点 C 完全背对太阳,不应接收到任何光线。
注: 在现实世界中,光线从一个表面反射到另一个表面;如果房间内有任何光线,则物体会有一些反射,这些反射会柔和地照亮所有其他物体的背面。这是全局光照。Phong 光照模型单独照亮每个对象,称为局部光照。
图中的虚线与表面相切。切线是最能描述曲线在某一点处斜率的直线。
从圆中出来的线与切线成直角。这些称为表面法线,您第一次遇到这些法线是在第 7 章 “片段函数”中。
光照类型
计算机图形学中有几个标准的光源选项,每个选项都起源于现实世界。
• Directional Light:沿单个方向发送光线。太阳是定向光。
• Point Light:像灯泡一样向各个方向发送光线。
• Spotlight:向圆锥体定义的有限方向发送光线。手电筒或台灯将是聚光灯。
定向光
一个场景中可以有许多光照。事实上,在工作室摄影中,只有一个灯光是非常不寻常的。通过将灯光放入场景中,可以控制阴影落下的位置和黑暗程度。在本章中,您将向场景添加多个灯光。
您将创建的第一种光照是太阳。太阳是向各个方向发射光线的点光源,但对于计算机建模,您可以将其视为定向光。它是一个遥远的强大光源。当光线到达地球时,光线似乎是平行的。在阳光明媚的日子里在外面检查一下——你所看到的一切都有它的影子,朝着同一个方向移动。
要定义光源类型,您需要创建一个 GPU 和 CPU 都可以读取的 Light 结构体,以及一个描述 GameScene 光照的 SceneLighting 结构体。
➤ 在 Shaders 组中,打开 Common.h,在 #endif 之前,创建您将使用的光源类型的枚举:
typedef enum {
unused = 0,
Sun = 1,
Spot = 2,
Point = 3,
Ambient = 4
} LightType;
➤ 在此之下,添加定义光源的结构体:
typedef struct {
LightType type;
vector_float3 position;
vector_float3 color;
vector_float3 specularColor;
float radius;
vector_float3 attenuation;
float coneAngle;
vector_float3 coneDirection;
float coneAttenuation;
} Light;
此结构体保存光的位置和颜色。在学习本章时,您将了解其他属性。
➤ 在 Game 组中创建一个新的 Swift 文件,并将其命名为 SceneLighting.swift。然后,添加以下内容:
struct SceneLighting {
static func buildDefaultLight() -> Light {
var light = Light()
light.position = [0, 0, 0]
light.color = [1, 1, 1]
light.specularColor = [0.6, 0.6, 0.6]
light.attenuation = [1, 0, 0]
light.type = Sun
return light
}
}
此文件将保存 GameScene 的光照。您将拥有多个光源,buildDefaultLight() 将创建一个基本光源。
➤ 在 SceneLighting 中为太阳定向光源创建一个属性:
let sunlight: Light = {
var light = Self.buildDefaultLight()
light.position = [1, 2, -2]
return light
}()
position 在世界空间中。这会在场景的右侧,球体的前方放置一个光源。球体将放置在世界的原点处。
➤ 创建一个数组来保存您即将创建的各种光源:
var lights: [Light] = []
➤ 添加初始化器:
init() {
lights.append(sunlight)
}
您将在初始化器中添加场景的所有灯光。
➤ 打开 GameScene.swift,并将 lighting 属性添加到 GameScene:
let lighting = SceneLighting()
您将在 fragment 函数中执行所有光照着色,因此您需要将光源数组传递给该函数。Metal Shading Language 没有动态数组功能,因此无法找出数组中的项目数。您将此值传递给 Params 中的片段着色器。
➤ 打开 Common.h,并将这些属性添加到 Params 中:
uint lightCount;
vector_float3 cameraPosition;
稍后您将需要 Camera Position 属性。
在 Common.h 中向 BufferIndices 添加新索引:
LightBuffer = 13
您将使用此索引将光照详细信息发送到 fragment 函数。
➤ 打开 Renderer.swift,并将其添加到 updateUniforms(scene:) 中:
params.lightCount = UInt32(scene.lighting.lights.count)
您将能够在片段着色器函数中访问此值。
➤ 在 draw(scene:in:) 中,在 scene.models 中 model 之前,添加以下内容:
var lights = scene.lighting.lights
renderEncoder.setFragmentBytes(
&lights,
length: MemoryLayout<Light>.stride * lights.count,
index: LightBuffer.index)
在这里,您将索引13缓冲区中的灯光数组发送到 fragment 函数。
您现在已经在 Swift 端设置了一个太阳光。您将在 fragment 函数中执行所有实际的光照计算,并了解有关光照属性的更多信息。
Phong反射模型
在 Phong 反射模型中,有三种类型的光照反射。您将计算每个颜色,然后将它们相加以生成最终颜色。
• 漫反射:理论上,照射到表面的光线会以围绕该点的表面法线反射的角度反射。然而,表面在微观上是粗糙的,因此光线会向各个方向反射,如上图所示。这将产生漫反射颜色,其中光强度与入射光与曲面法线之间的角度成正比。在计算机图形学中,这个模型被称为朗伯反射率,以 1777 年去世的约翰·海因里希·兰伯特 (Johann Heinrich Lambert) 的名字命名。在现实世界中,这种漫反射通常适用于暗淡、粗糙的表面,但具有最朗伯特性的表面是人造的:Spectralon (https://en.wikipedia.org/wiki/Spectralon),用于光学元件。
• Specular:表面越光滑,越闪亮,并且光线从表面反射的方向就越少。镜子完全从表面法线反射,没有偏转。闪亮的物体会产生可见的镜面高光,渲染镜面反射可以让观众了解物体的表面类型,无论汽车是旧车残骸还是刚从销售地新鲜出炉。
• 环境光:在现实世界中,光线会到处反射,因此被遮蔽对象很少是全黑的。这是环境反射。
表面颜色由自发光物体表面颜色加上环境光、漫反射和镜面反射的贡献组成。对于漫反射和镜面反射,要找出表面在特定点应接收多少光,您只需找出入射光方向与表面法线之间的角度即可。
点乘
幸运的是,有一个简单的数学运算来发现两个向量之间的角度,称为点积。
和:
其中 ||A||表示向量 A 的长度(或大小)。
更幸运的是,simd 和 Metal Shading Language 都有一个函数 dot()来获取点积,因此您不必记住公式。
除了找出两个向量之间的角度外,您还可以使用点积来检查两个向量是否指向同一方向。
将两个向量的大小调整为单位向量 — 即长度为 1 的向量。您可以使用 normalize() 函数执行此操作。如果两个单位向量平行且指向同一个方向,则点积结果将为 1。如果它们是平行的但方向相反,则结果将为 -1。如果它们成直角(正交),则结果将为 0。
看上图,如果黄色(太阳)向量垂直向下,蓝色(法线)向量垂直向上,则点积将为 -1。该值是两个向量之间的余弦角。余弦的优点在于它们的值始终是介于 -1 和 1 之间,因此您可以使用此范围来确定光线在某个点的亮度。
以下面示例为例:
太阳从天而降,方向矢量为 [2, -2, 0]。向量 A 是 [-2, 2, 0] 的法向量。这两个向量指向相反的方向,因此当您将向量转换为单位向量(归一化它们)时,它们的点积将为 -1。
向量 B 是 [0.3, 2, 0] 的法向量。太阳光是定向光,因此使用相同的方向向量。归一化后,太阳光和 B 的点积为 -0.59。
此 Playground 代码演示了计算。
注意:第 8 行之后的结果表明,使用浮点时应始终小心,因为结果永远不会精确。切勿使用表达式,例如 if (x == 1.0) - 应始终使用<= 或 >=检查。
在片段着色器中,您将能够获取这些值,并使用点乘乘以片段颜色,以获得片段的亮度。
漫反射
从太阳光中着色,并不取决于摄像机的位置。旋转场景时,将旋转世界,包括太阳。太阳的位置将位于世界空间中,您需要将模型的法线放入同一世界空间,以便能够根据太阳光方向计算点积。事实上,我们可以选择任何空间,只需要把两个点乘的向量变换到同一个空间即可。
为了能够在 fragment 函数中评估表面的坡度,您需要重新定位 vertex 函数中的法线,其方式与之前重新定位顶点位置的方式大致相同。您需要将法线添加到顶点描述符中,以便顶点函数可以处理它们。
➤ 打开 ShaderDefs.h,并将这些属性添加到 VertexOut 中:
float3 worldPosition;
float3 worldNormal;
它们将持有世界空间中的顶点位置和顶点法线。
计算法线的新位置与顶点位置计算略有不同。MathLibrary.swift 包含一个 matrix 方法,用于从另一个矩阵创建普通矩阵。这个法线矩阵是一个 3×3 矩阵,因为首先,您将在不需要投影的世界空间中进行光照,其次,平移对象不会影响法线的斜率。因此,您不需要第四个 W 维度。但是,如果沿一个方向(非线性)缩放对象,则对象的法线不再是正交的,因此此方法将不起作用。只要你决定你的引擎不允许非线性缩放,那么你可以使用模型矩阵左上角的 3×3 部分,这就是你在这里要做的。
➤ 打开 Common.h 并将此矩阵属性添加到 Uniforms:
matrix_float3x3 normalMatrix;
这将在世界空间中保存法线矩阵。
➤ 在 Game 组中,打开 Rendering.swift,在render(encoder:uniforms:params:)中设置 uniforms.modelMatrix:后添加这个
uniforms.normalMatrix = uniforms.modelMatrix.upperLeft
这将从模型矩阵创建法线矩阵。
➤ 打开 Vertex.metal,在 vertex_main 中,分配position后,添加以下内容:
float4 worldPosition = uniforms.modelMatrix * in.position;
在转换为摄像机和投影空间之前,您可以保持顶点的世界位置。
➤ 定义 out 时,填充 VertexOut 属性:
.worldPosition = worldPosition.xyz / worldPosition.w,
.worldNormal = uniforms.normalMatrix * in.normal
光栅器按position执行透视划分,如第 6 章 “坐标空间”中所述。要确保处理所有缩放问题,请在此处对 worldPosition除以 w。
在本章的前面部分,您将 LightBuffer索引缓冲区中Renderer 的 lights 数组发送到fragment 函数,但您尚未更改 fragment 函数以接收该数组。
➤ 打开 Fragment.metal 并将以下内容添加到 fragment_main 的参数列表中:
constant Light *lights [[buffer(LightBuffer)]],
使用c++创建着色函数
通常,您需要从多个文件访问 C++ 函数。光照函数是您可能希望分离出来的一些函数的一个很好的示例,因为您可以拥有各种光照模型,这些模型可能会调用一些相同的代码。
要从多个 .metal 文件中调用函数:
1. 使用要创建的函数的名称设置头文件。
2. 创建一个新的 .metal 文件并导入头文件,如果您打算使用该文件中的结构体,则还要导入桥接头文件 Common.h。
3. 在此新文件中创建光照函数。
4. 在现有的 .metal 文件中,导入新的头文件并使用光照函数。
在 Shaders 组中,创建一个名为 Lighting.h 的新 Header File。不要将其添加到目标中。
➤ 在 #endif /* 之前添加此函数头 Lighting_h */:
#import "Common.h"
float3 phongLighting(
float3 normal,
float3 position,
constant Params ¶ms,
constant Light *lights,
float3 baseColor);
在这里,您将定义一个将返回 float3 的 C++ 函数。
在 Shaders 组中,创建一个名为 Lighting.metal 的新 Metal 文件。将其添加到target。
➤ 添加此新功能:
#import "Lighting.h"
float3 phongLighting(
float3 normal,
float3 position,
constant Params ¶ms,
constant Light *lights,
float3 baseColor) {
return float3(0);
}
您创建一个返回float3 零值的新函数。您将在 phongLighting 中构建代码来计算这个最终的光照值。
➤ 打开 Fragment.metal,#import “Common.h” 替换为:
#import "Lighting.h"
现在,您将能够在此文件中使用 phongLighting。
➤ 在 fragment_main 中,替换 return float4(baseColor, 1);为:
float3 normalDirection = normalize(in.worldNormal);
float3 color = phongLighting(
normalDirection,
in.worldPosition,
params,
lights,
baseColor );
return float4(color, 1);
在这里,您将世界法线设置为单位向量,并使用必要的参数调用新的光照函数。
如果您现在构建并运行该应用程序,您的模型将呈现为黑色,因为这是您当前从 phongLighting 返回的颜色。
➤ 打开 Lighting.metal,并替换 return float3(0);为:
float3 diffuseColor = 0;
float3 ambientColor = 0;
float3 specularColor = 0;
for (uint i = 0; i < params.lightCount; i++) {
Light light = lights[i];
switch (light.type) {
case Sun: {
break; }
case Point: {
break; }
case Spot: {
break; }
case Ambient: {
break; }
case unused: {
break; }
} }
return diffuseColor + specularColor + ambientColor;
这将为您创建计算所有光照的轮廓。您将累积得到最终的片段颜色,由漫反射、镜面反射和环境光组成。
➤ 在 case Sun 的break前面,添加以下内容:
// 1
float3 lightDirection = normalize(-light.position);
// 2
float diffuseIntensity =
saturate(-dot(lightDirection, normal));
// 3
diffuseColor += light.color * baseColor * diffuseIntensity;
浏览此代码:
1. 将光线的方向设为单位向量。
2. 计算两个向量的点积。当片段完全指向光线时,点积将为 -1。进一步计算使此值为正更容易,因此您取点积的负值。saturate 通过限制负数来确保该值介于 0 和 1 之间。这为您提供了表面的斜率,从而提供了漫反射因子的强度。
3. 将基础颜色乘以漫反射强度,以获得漫反射着色。如果有多个太阳光,则 diffuseColor 将累积漫反射着色。
➤ 构建并运行应用程序。
您可以通过从 phongLighting 返回中间计算来对结果进行健全性检查。下图显示了前视图中的 normal 和 diffuseIntensity。
注意:要在应用程序中获取前视图,请在运行时按 Alpha 键上方的“1”。“2” 将重置为默认视图。
Utility 组中的 DebugLights.swift 和 DebugLights.metal 具有一些调试方法,以便您可以直观地了解光源的位置。
➤ 打开 DebugLights.swift,并删除文件顶部和底部的 /* 和 */。在本章中添加代码之前,此文件不会编译,但现在可以编译。
➤ 打开 Renderer.swift,在 draw(scene:in:) 的末尾,在 renderEncoder.endEncoding() 之前,添加以下内容:
DebugLights.draw(
lights: scene.lighting.lights,
encoder: renderEncoder,
uniforms: uniforms)
此代码将显示线条以可视化太阳光的方向。
➤ 构建并运行应用程序。
红线显示平行太阳光方向矢量。旋转场景时,可以看到最亮的部分是面向太阳的部分。
注意:调试方法使用 .line 作为渲染类型。遗憾的是,线宽在 GPU 上是不可配置的,它们可能会在某些角度,因线条太细而无法渲染时消失。
这个着色效果令人愉悦,但不准确。看看球体的背面。球体的背面是黑色的;但是,您可以看到绿色环绕的顶部是亮绿色的,因为它朝上。在现实世界中,周围环境会被球体阻挡,因此处于阴影中。但是,您目前没有考虑遮挡,只有在第 13 章 “阴影” 中掌握阴影后才考虑这个问题。
环境反射
在现实世界中,颜色很少是纯黑色。到处都是光线反射。要模拟这种情况,您可以使用环境光照。您将找到场景中灯光的平均颜色,并将其应用于场景中的所有表面。
➤ 打开 SceneLighting.swift,并添加一个 ambient light 属性:
let ambientLight: Light = {
var light = Self.buildDefaultLight()
light.color = [0.05, 0.1, 0]
light.type = Ambient
return light
}()
此光照略带绿色。
➤ 将以下内容添加到 init() 的末尾:
lights.append(ambientLight)
➤ 打开 Lighting.metal,case Ambient的break上面,添加以下内容:
ambientColor += light.color;
➤ 构建并运行应用程序。场景现在呈绿色,就像有绿灯在周围反射一样。
镜面反射
在 Phong 反射模型中,最后但并非最不重要的一点是镜面反射。您现在有机会在球体上涂上一层闪亮的清漆。镜面高光取决于观察者的位置。如果您经过一辆闪亮的汽车,您只会在某些角度看到高光。
光线进入 (L) 并被绕着法线 (N)反射 (R) 。如果观察者 (V) 位于反射 (R) 周围的特定圆锥体内,则观察者将看到镜面高光。该圆锥体是指数光泽度参数。表面越闪亮,镜面反射高光就越小、越强烈。
在本例中,观察者是您的相机,因此您需要将相机坐标再次传递到 fragment 函数。之前,您在 params 中设置了一个 cameraPosition 属性,您将使用它来传递相机位置。
➤ 打开 Renderer.swift,然后在 updateUniforms(scene:) 中添加以下内容:
params.cameraPosition = scene.camera.position
scene.camera.position 已在世界空间中,并且您已将参数传递给 fragment 函数,因此您无需在此处执行进一步作。
➤ 打开 Lighting.metal,然后在 phongLighting 中,将以下变量添加到函数顶部:
float materialShininess = 32;
float3 materialSpecularColor = float3(1, 1, 1);
它们包含光泽度因子和镜面反射颜色的表面材质属性。由于这些是表面属性,您应该从每个模型的材质中获取这些值,您将在下一章中执行此作。
➤ 在 case Sun的break前面添加以下内容:
if (diffuseIntensity > 0) {
// 1 (R)
float3 reflection =
reflect(lightDirection, normal);
// 2 (V)
float3 viewDirection =
normalize(params.cameraPosition);
// 3
float specularIntensity =
pow(saturate(dot(reflection, viewDirection)),
materialShininess);
specularColor +=
light.specularColor * materialSpecularColor
* specularIntensity;
}
浏览此代码:
1. 为了计算镜面反射颜色,您需要 (L) ight、(R) eflection、(N) ormal 和 (V)iew。您已经有 (L) 和 (N),因此在这里使用 Metal Shading Language 函数 reflect 来获取 (R)。
2. 您需要 (V) 片段和相机之间的视图向量。
3. 现在,您计算镜面反射强度。您可以使用点积找到反射和视图之间的角度,使用 saturate 将结果限制在 0 和 1 之间,并使用 pow 将结果提高到光泽度。然后,您可以使用此强度来确定片段的镜面反射颜色。
➤ 构建并运行应用程序以查看您完成的照明。
尝试将材质光泽度从 2 更改为 1600。在第11章“地图和材质”中,您将了解如何从模型中读取材质和纹理属性以更改其颜色和照明。
您已经为太阳光创建了足够逼真的光照情况。您可以使用点光源和聚光灯为场景添加更多变化。
点光源
在太阳光中,您将位置转换为平行方向向量。与太阳光相反,而点光源则向所有方向发射光线。
灯泡只会照亮一定半径的区域,超过该半径后,一切都是黑暗的。因此,您还将指定光线不会无限传播的衰减。
光线衰减可以突然或逐渐发生。衰减的原始 OpenGL 公式为:
其中 x 是常数衰减因子,y 是线性衰减因子,z 是二次衰减因子。
该公式给出了弯曲的衰减。您将用 float3 表示 xyz。完全没有衰减将是 float3(1, 0, 0) — 将 x、y 和 z 代入公式得到值 1。
➤ 打开 SceneLighting.swift,并向 SceneLighting 添加点光源属性:
let redLight: Light = {
var light = Self.buildDefaultLight()
light.type = Point
light.position = [-0.8, 0.76, -0.18]
light.color = [1, 0, 0]
light.attenuation = [0.5, 2, 1]
return light
}()
聚光
在本章中,您将创建的最后一种光源类型是聚光灯。这会向有限的方向发送光线。想想手电筒,光线从一个小点发出,但当它照射到地面时,它是一个更大的椭圆。
您可以定义圆锥体角度以包含具有圆锥体方向的光线。我们还要定义一个衰减因子来控制圆锥靠边的光的衰减。
打开 SceneLighting.swift,添加一个新的光照对象:
lazy var spotlight: Light = {
var light = buildDefaultLight()
light.position = [0.4, 0.8, 1]
light.color = [1, 0, 1]
light.attenuation = float3(1, 0.5, 0)
light.type = Spotlight
light.coneAngle = Float(40).degreesToRadians
light.coneDirection = [-2, 0, -1.5]
light.coneAttenuation = 12
return light
}()
本光照和点光源有点类似,不过增加了圆锥角度,方向,以及圆锥衰减因子。
在init(metalView) 添加这个光照到光照数组中。
lights.append(spotlight)
打开 Lighting.metal,然后在 phongLighting 中,在 case Spot 的 break 上方添加以下代码:
// 1
float d = distance(light.position, position);
float3 lightDirection = normalize(light.position - position);
// 2
float3 coneDirection = normalize(light.coneDirection);
float spotResult = dot(lightDirection, -coneDirection);
// 3
if (spotResult > cos(light.coneAngle)) {
float attenuation = 1.0 / (light.attenuation.x +
light.attenuation.y * d + light.attenuation.z * d * d);
// 4
attenuation *= pow(spotResult, light.coneAttenuation);
float diffuseIntensity =
saturate(dot(lightDirection, normal));
float3 color = light.color * baseColor * diffuseIntensity;
color *= attenuation;
diffuseColor += color;
}
此代码与点光源代码非常相似。浏览评论:
1. 计算距离和方向,就像计算点光源一样。这束光可能在聚光锥体外。
2. 计算该光线方向与聚光灯指向的方向之间的余弦角(即点积)。
3. 如果该结果超出圆锥角,则忽略该射线。否则,请计算点光源的衰减。指向同一方向的向量的点积为 1.0。
4. 使用 coneAttenuation 作为幂来计算聚光灯边缘的衰减。
➤ 构建并运行应用程序。
尝试更改各种衰减值。锥体角度为 5°,然后衰减向量为(1, 0, 0),锥体衰减因子为 1000 将产生非常小的聚焦柔光;而 20°的锥体角度和 1 的锥体衰减因子将产生锐利的圆形光。
参考
https://zhuanlan.zhihu.com/p/391592709
https://zhuanlan.zhihu.com/p/392622099