Metal学习笔记十:光照基础

news2025/3/5 7:19:03

光和阴影是使场景流行的重要要求。通过一些着色器艺术,您可以突出重要的对象、描述天气和一天中的时间并设置场景的气氛。即使您的场景由卡通对象组成,如果您没有正确地照亮它们,场景也会变得平淡无奇。

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

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

点乘

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


和:


其中 ||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 = Ambient
  return 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);
  // 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

报告分享 | 哈工大赛尔实验室——大模型时代的具身智能

本报告详细介绍了大模型时代的具身智能&#xff0c;探讨了智能机器人的发展历程、技术挑战和未来发展方向。&#xff08; 报告全文下载&#xff1a;具身大模型关键技术与应用&#xff08;哈尔滨工业大学社会计算与信息检索研究中心&#xff09;.pdf&#xff01;&#xff09;

第四十一:Axios 模型的 get ,post请求

Axios 的 get 请求方式 9.双向数据绑定 v-model - 邓瑞编程 Axios 的 post 请求方式&#xff1a;

全国青少年航天创新大赛各项目对比分析

全国青少年航天创新大赛各项目对比分析 一、比赛场地对比 项目名称场地尺寸场地特点组别差异筑梦天宫虚拟三维场景动态布局&#xff0c;小学组3停泊处&#xff0c;初高中组6停泊处&#xff1b;涉及传送带、机械臂、传感器等虚拟设备。初中/高中组任务复杂度更高&#xff0c;运…

20250304在Ubuntu20.04的GUI下格式化exFAT格式的TF卡为ext4格式

20250304在Ubuntu20.04的GUI下格式化exFAT格式的TF卡为ext4格式 2025/3/4 16:47 缘起&#xff1a;128GB的TF卡&#xff0c;只能格式化为NTFS/exFAT/ext4。 在飞凌的OK3588-C下&#xff0c;NTFS格式只读。 exFAT需要改内核来支持。 现在只剩下ext4了。 linux R4默认不支持exFAT…

服务器配置-从0到分析4:ssh免密登入

该部分涉及到公钥、私钥等部分knowledge&#xff0c;本人仅作尝试 若将本地机器 SSH Key 的公钥放到远程主机&#xff0c;就能无需密码直接远程登录远程主机 1&#xff0c;在客户端生成 ssh 公私钥&#xff1a; 也就是我们本地机器&#xff0c;windows电脑 一路回车即可&am…

React 组件基础介绍

基本概念&#xff1a;一个组件就是用户界面的一部分&#xff0c;可以有自己的逻辑和外观&#xff0c;组件之间可以互相嵌套、复用多次。每个组件就是一个首字母大写的函数&#xff0c;内部存放了组件的逻辑和试图UI&#xff0c;渲染组件只需要把组件 当成 标签 书写。App 可以视…

环境变量 ─── linux第14课

本内容为总结: 1. 环境变量本质是配置信息, 在系统配置时起效 . 2. 环境变量具有全局性(子进程可以继承父进程的环境信息,不能继承本地变量) 3. 进程具有独立性 ,环境变量可以进程间传递信息(只读信息) 环境变量 环境变量(environment variables)一般是指在操作系统中用来指定操…

基于APDL语言的结构优化设计

1、前言 结构设计是创造结构方案的过程&#xff0c;传统的结构设计是设计者按设计要求和设计者的实践经验&#xff0c;参考类似工程&#xff0c;通过判断创造结构方案&#xff0c;然后进行力学分析或按规范要求作安全校核&#xff0c;再修改设计。 而结构优化设计与分析则把力…

一、MySQL备份恢复

一、MySQL备份恢复 1.1 MySQL日志管理 数据库中数据丢失或被破坏可能原因 误删除数据库 数据库工作时&#xff0c;意外断电或程序意外终止 由于病毒造成的数据库损坏或丢失 文件系统损坏后&#xff0c;系统进行自检操作 升级数据库时&#xff0c;命令语句不严格 设备故…

【Linux第三弹】Linux基础指令 (下)

目录 &#x1f31f;1.find指令 1.1find使用实例 ​编辑 &#x1f31f;2.which指令 &#x1f31f;3.grep指令 3.1grep使用实例 &#x1f31f; 4.zip/unzip指令 4.1 zip/unzip使用实例 &#x1f31f;5.tar指令 5.1 tar使用实例 &#x1f31f;6.完结 很庆幸走在自己…

VB6网络通信软件开发,上位机开发,TCP网络通信,读写数据并处理,完整源码下载

VB6网络通信软件开发&#xff0c;上位机开发&#xff0c;TCP网络通信&#xff0c;读写数据并处理&#xff0c;完整源码下载 完整源码XZ网口四进四出主动上传版_VB源代码.rar 下载链接&#xff1a;http://xzios.cn:86/WJGL/DownLoadDetial?Id20 在自动化、物联网以及工业控制…

TMS320F28P550SJ9学习笔记1:CCS导入工程以及测试连接单片机仿真器

学习记录如何用 CCS导入工程以及测试连接单片机仿真器 以下为我的CCS 以及驱动库C2000ware 的版本 CCS版本&#xff1a; Code Composer Studio 12.8.1 C2000ware &#xff1a;C2000Ware_5_04_00_00 目录 CCS导入工程&#xff1a; 创建工程&#xff1a; 添加工程&#xff1a; C…

阿里万相,正式开源

大家好&#xff0c;我是小悟。 阿里万相正式开源啦。这就像是AI界突然开启了一扇通往宝藏的大门&#xff0c;而且还是免费向所有人敞开的那种。 你想想看&#xff0c;在这个科技飞速发展的时代&#xff0c;AI就像是拥有神奇魔法的魔法师&#xff0c;不断地给我们带来各种意想…

纯前端使用 Azure OpenAI Realtime API 打造语音助手

本文手把手教你如何通过纯前端代码实现一个实时语音对话助手&#xff0c;结合 Azure 的 Realtime API&#xff0c;展示语音交互的未来形态。项目开源地址&#xff1a;https://github.com/sangyuxiaowu/WssRealtimeAPI 1. 背景 在这个快节奏的数字时代&#xff0c;语音助手已经…

基于Windows11的RAGFlow安装方法简介

基于Windows11的RAGFlow安装方法简介 一、下载安装Docker docker 下载地址 https://www.docker.com/ Download Docker Desktop 选择Download for Winodws AMD64下载Docker Desktop Installer.exe 双点击 Docker Desktop Installer.exe 进行安装 测试Docker安装是否成功&#…

教育强国建设“三年行动计划“分析

教育部即将推出的教育强国建设"三年行动计划"中&#xff0c;职业教育板块的部署体现出鲜明的战略导向和创新思维&#xff0c;其核心是通过系统化布局和结构性改革推动职业教育高质量发展。以下从政策内涵、实施路径及潜在影响三个维度展开分析&#xff1a; 一、政策…

基于Spring Boot+vue的厨艺交流平台系统设计与实现

大家好&#xff0c;今天要和大家聊的是一款基于Spring Boot的“厨艺交流平台”系统的设计与实现。项目源码以及部署相关事宜请联系我&#xff0c;文末附上联系方式。 项目简介 基于Spring Boot的“厨艺交流平台”系统设计与实现的主要使用者分为管理员、普通用户和游客。没有…

GPU、NPU与LPU:大语言模型(LLM)硬件加速器全面对比分析

引言&#xff1a;大语言模型计算基础设施的演进 随着大语言模型&#xff08;LLM&#xff09;的快速发展与广泛应用&#xff0c;高性能计算硬件已成为支撑LLM训练与推理的关键基础设施。目前市场上主要有三类处理器用于加速LLM相关任务&#xff1a;GPU&#xff08;图形处理单元…

强化学习-随机近似与随机梯度下降

强化学习-数学理论 强化学习-基本概念强化学习-贝尔曼公式强化学习-贝尔曼最优公式强化学习-值迭代与策略迭代强化学习-蒙特卡洛方法强化学习-随机近似于随机梯度下降 文章目录 强化学习-数学理论一、前言二、再谈mean eatimation2.1 回顾蒙特卡洛法2.2 新角度解决求均值问题2…

Linux纯命令行界面下SVN的简单使用教程

诸神缄默不语-个人技术博文与视频目录 我用的VSCode插件是这个&#xff1a; 可以在文件中用色块显示代码修改了什么地方&#xff0c;点击色块还可以显示修改内容。 文章目录 1. SVN安装2. checkout3. update1. 将文件加入版本控制 4. commit5. 查看SVN信息&#xff1a;info6.…