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 = Sunreturn 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),用于光学元件。
• 镜面反射:表面越光滑,越闪亮,并且光线从表面反射的方向就越集中。镜子完全从表面法线反射,没有偏转。闪亮的物体会产生可见的镜面高光,渲染镜面反射可以让观众了解物体的表面类型,无论汽车是旧车残骸还是刚从销售地新鲜出炉。
• 环境光:在现实世界中,光线会到处反射,因此被遮蔽对象很少是全黑的。这是环境反射。

表面颜色由自发光物体表面颜色加上环境光、漫反射和镜面反射的贡献组成。对于漫反射和镜面反射,要找出表面在特定点应接收多少光,您只需找出入射光方向与表面法线之间的角度即可。

点乘

幸运的是,有一个简单的数学运算来发现两个向量之间的角度,称为点积。


和:


其中 ||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。不要将其添加到target中。
➤ 在 #endif /* Lighting_h */ 之前添加此函数头:

#import "Common.h"
float3 phongLighting(float3 normal,float3 position,constant Params &params,constant Light *lights,float3 baseColor);

在这里,您定义一个返回 float3 的 C++ 函数。
在 Shaders 组中,创建一个名为 Lighting.metal 的新 Metal 文件。将其添加到target。
➤ 添加此新函数:

#import "Lighting.h"
float3 phongLighting(float3 normal,float3 position,constant Params &params,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 = Ambientreturn light
}()

此光照略带绿色。

➤ 将以下内容添加到 init() 的末尾:

   lights.append(ambientLight)

➤ 打开 Lighting.metal,case Ambient的break上面,添加以下内容:

ambientColor += light.color;

➤ 构建并运行应用程序。场景现在呈绿色,就像有绿灯在周围反射一样。

镜面反射

在 Phong 反射模型中,最后但并非最不重要的一点是镜面反射。您现在有机会在球体上涂上一层闪亮的清漆。镜面高光取决于观察者的位置。如果您经过一辆闪亮的汽车,您只会在某些角度看到高光。

光线进入 (L) 并被绕着法线 (N)反射 (R) 。如果观察者 (V) 位于反射 (R) 周围的特定圆锥体内,则观察者将看到镜面高光。cone是指数光泽度参数。表面越闪亮,镜面反射高光就越小、越强烈。
在本例中,观察者是您的相机,因此您需要将相机坐标再次传递到 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);// 3float 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 = Pointlight.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 = Spotlightlight.coneAngle = Float(40).degreesToRadianslight.coneDirection = [-2, 0, -1.5]light.coneAttenuation = 12return 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);
// 4attenuation *= 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/27851.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

动态规划_路径问题(典型算法思想)—— OJ例题算法解析思路

目录 一、62. 不同路径 - 力扣&#xff08;LeetCode&#xff09; 算法代码&#xff1a; 代码思路分析 问题定义&#xff1a; 动态规划定义&#xff1a; 边界条件&#xff1a; 填表过程&#xff1a; 返回结果&#xff1a; 代码优化思路 空间优化&#xff1a; 滚动数组…

【AI论文】ViDoRAG:通过动态迭代推理代理实现视觉文档检索增强生成

摘要&#xff1a;理解富含视觉信息的文档中的信息&#xff0c;对于传统的检索增强生成&#xff08;Retrieval-Augmented Generation&#xff0c;RAG&#xff09;方法来说&#xff0c;仍然是一个重大挑战。现有的基准测试主要集中在基于图像的问答&#xff08;Question Answerin…

【赵渝强老师】监控Redis

对运行状态的Redis实例进行监控是运维管理中非常重要的内容&#xff0c;包括&#xff1a;监控Redis的内存、监控Redis的吞吐量、监控Redis的运行时信息和监控Redis的延时。通过Redis提供的监控命令便能非常方便地实现对各项指标的监控。 一、监控Redis的内存 视频讲解如下 【…

HTML前端手册

HTML前端手册 记录前端框架在使用过程中遇到的各种问题和解决方案&#xff0c;供后续快速进行手册翻阅使用 文章目录 HTML前端手册1-前端框架1-TypeScript框架2-CSS框架 2-前端Demo1-Html常用代码 2-知云接力3-Live2D平面动画 3-前端运维1-NPM版本管理 1-前端框架 1-TypeScrip…

C++:类和对象(下篇)

1. 再谈构造函数 1.1 构造函数体赋值 在创建对象时&#xff0c;编译器通过调用构造函数&#xff0c;给对象中各个成员变量一个合适的初始值。 class Date { public:Date(int year, int month, int day){_year year;_month month;_day day;} private:int _year;int _mont…

JVM基本概念及内存管理模型

一、JVM基本概念 JVM&#xff08;Java Virtual Machine&#xff0c;Java 虚拟机&#xff09;是 Java 程序运行的核心组件。它负责将 Java 字节码转换为特定平台的机器指令&#xff0c;并提供内存管理、垃圾回收、安全性等功能。JVM 的主要功能包括以下&#xff1a; 加载和执行…

MyBatis - 单元测试 参数传递 注解 CRUD

目录 1. MyBatis 简介 2. 简单使用 MyBatis 2.1 创建 MyBatis 项目 2.2 连接数据库 2.3 创建 Java 类 2.4 创建 Mapper 接口 2.5 在测试类中执行 3. 单元测试 3.1 Test 3.2 SpringBootTest 3.3 BeforeEach / AfterEach 4. MyBatis 基础操作 4.1 配置 MyBatis 打印日…

大语言模型学习--本地部署DeepSeek

本地部署一个DeepSeek大语言模型 研究学习一下。 本地快速部署大模型的一个工具 先根据操作系统版本下载Ollama客户端 1.Ollama安装 ollama是一个开源的大型语言模型&#xff08;LLM&#xff09;本地化部署与管理工具&#xff0c;旨在简化在本地计算机上运行和管理大语言模型…

shell文本处理

shell文本处理 一、grep ​ 过滤来自一个文件或标准输入匹配模式内容。除了 grep 外&#xff0c;还有 egrep、fgrep。egrep 是 grep 的扩展&#xff0c;相当于 grep -E。fgrep 相当于 grep -f&#xff0c;用的比较少。 用法 grep [OPTION]... PATTERN [FILE]...支持的正则描述…

Linux中死锁问题的探讨

在 Linux 中&#xff0c;死锁&#xff08;Deadlock&#xff09; 是指多个进程或线程因为竞争资源而相互等待&#xff0c;导致所有相关进程或线程都无法继续执行的状态。死锁是一种严重的系统问题&#xff0c;会导致系统资源浪费&#xff0c;甚至系统崩溃。 死锁的定义 死锁是指…

Baklib内容中台赋能企业智管

内容中台构建全场景智管 现代企业数字化运营中&#xff0c;全域内容管理能力已成为核心竞争力。通过智能知识引擎驱动的内容中台架构&#xff0c;企业能够实现跨部门、多形态数据的统一归集与动态调度。以某制造企业为例&#xff0c;其利用中台系统将分散在CRM、ERP及内部文档…

ArcGIS Pro高级应用:高效生成TIN地形模型

一、引言 在地理信息科学与遥感技术的快速发展背景下&#xff0c;数字高程模型&#xff08;DEM&#xff09;已成为地形表达与分析的关键工具。 三角网&#xff08;TIN&#xff09;作为DEM的一种重要形式&#xff0c;因其能够精准描绘复杂地形特征而广受青睐。 ArcGIS Pro为用…

leetcode112-路径总和

leetcode 112 思路 我们利用递归来实现&#xff0c;用result字段来记录结果值&#xff0c;默认为false&#xff0c;我们递归的时候传入需要的目标值&#xff0c;然后每次遍历到一个节点&#xff0c;就用目标值减去节点当前值&#xff0c;最终到叶子节点时&#xff0c;如果是…

LLM | 论文精读 | CVPR | PEACE : 通过多模态大语言模型(MLLMs)赋能地质图全面理解

论文标题&#xff1a;FairCLIP: Harnessing Fairness in Vision-Language Learning 作者&#xff1a;Yan Luo Min Shi Muhammad Osama Khan Muhammad Muneeb Afzal等 期刊&#xff1a;CVPR 2025 email&#xff1a;yuhan.huangwhu.edu.cn 创作不易&#xff0c;恳请大家点赞收…

网络学习(四)HTTPS中,SSL的单向认证与双向认证

目录 一、什么是SSL&#xff1f;1.1 SSL 的主要功能1.2 SSL 的工作原理1.3 SSL 的核心组件1.4 SSL 的应用场景1.5 SSL 与 TLS 的区别 二、SSL 单向认证、双向认证2.1 SSL 单向认证2.2 SSL 双向认证2.3 总结&#xff1a;SSL 单向认证和双向认证的区别 一、什么是SSL&#xff1f;…

Mybatis 中#{} 和${} 的区别是什么?

在 MyBatis 中&#xff0c;#{} 和 ${} 都是用于动态 SQL 语句中的占位符&#xff0c;但是它们的作用和使用方式是不同的。下面是它们的区别&#xff1a; 1. #{} —— 用于防止 SQL 注入和自动类型处理 #{} 是用来将参数安全地传递到 SQL 语句中&#xff0c;它会将传递的参数值…

HTML-05NPM使用踩坑

2025-03-04-NPM使用踩坑 本文讲述了一个苦逼程序员在使用NPM的时候突然来了一记nmp login天雷&#xff0c;然后一番折腾之后&#xff0c;终究还是没有解决npm的问题&#x1f61e;&#x1f61e;&#x1f61e;,最终使用cnpm完美解决的故事。 文章目录 2025-03-04-NPM使用踩坑[toc…

Zookeeper 的核心引擎:深入解析 ZAB 协议

#作者&#xff1a;张桐瑞 文章目录 前言ZAB 协议算法崩溃恢复选票结构选票筛选消息广播 前言 ZooKeeper 最核心的作用就是保证分布式系统的数据一致性&#xff0c;而无论是处理来自客户端的会话请求时&#xff0c;还是集群 Leader 节点发生重新选举时&#xff0c;都会产生数据…

C++ Primer 动态数组

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

基于 HTML、CSS 和 JavaScript 的智能九宫格图片分割系统

目录 1 前言 2 技术实现 2.1 HTML 结构 2.2 CSS 样式 2.3 JavaScript 交互 3 代码解析 3.1 HTML 部分 3.2 CSS 部分 3.3 JavaScript 部分 4 完整代码 5 运行结果 6 总结 6.1 系统特点 6.2 使用方法 1 前言 在当今数字化的时代&#xff0c;图片处理需求日益增长。…