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),用于光学元件。
• 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。不要将其添加到target中。
➤ 在 #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 = Ambientreturn 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);// 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
相关文章:

Metal学习笔记九:光照基础
光和阴影是使场景流行的重要要求。通过一些着色器艺术,您可以突出重要的对象、描述天气和一天中的时间并设置场景的气氛。即使您的场景由卡通对象组成,如果您没有正确地照亮它们,场景也会变得平淡无奇。 最简单的光照方法之一是 Phong 反射模…...

【字符串】最长公共前缀 最长回文子串
文章目录 14. 最长公共前缀解题思路:模拟5. 最长回文子串解题思路一:动态规划解题思路二:中心扩散法 14. 最长公共前缀 14. 最长公共前缀 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀,返回空字符…...

Linux提权之详细总结版(完结)
这里是我写了折磨多提权的指令的总结 我这里毫无保留分享给大家哦 首先神魔是提权 我们完整的渗透测试的流程是(个人总结的) 首先提升权限是我们拿到webshell之后的事情,如何拿到webshell,怎末才能拿到webshell,朋友们等我更新,持续更新中,下一篇更新的是windows提权 好了 废…...

week 3 - More on Collections - Lecture 3
一、Motivation 1. Java支持哪种类型的一维数据结构? Java中用于在单一维度中存储数据的数据结构,如arrays or ArrayLists. 2. 如何在Java下创建一维数据结构?(1-dimensional data structure) 定义和初始化这些一…...
Pwntools 的详细介绍、安装指南、配置说明
Pwntools:Python 开源安全工具箱 一、Pwntools 简介 Pwntools 是一个由 Security researcher 开发的 高效 Python 工具库,专为密码学研究、漏洞利用、协议分析和逆向工程设计。它集成了数百个底层工具的功能,提供统一的 Python API 接口&am…...

PLC(电力载波通信)网络机制介绍
1. 概述 1.1 什么是PLC 电力载波通讯即PLC,是英文Power line Carrier的简称。 电力载波是电力系统特有的通信方式,电力载波通讯是指利用现有电力线,通过载波方式将模拟或数字信号进行高速传输的技术。最大特点是不需要重新架设网络…...

Qt监控系统远程回放/录像文件远程下载/录像文件打上水印/批量多线程极速下载
一、前言说明 在做这个功能的时候,着实费了点心思,好在之前做ffmpeg加密解密的时候,已经打通了极速加密保存文件,主要就是之前的类中新增了进度提示信号,比如当前已经处理到哪个position位置,发个信号出来…...
自学微信小程序的第八天
DAY8 1、使用动画API即可完成动画效果的制作,先通过wx.createAnimation()方法获取Animation实例,然后调用Animation实例的方法实现动画效果。 表40:wx.createAnimation()方法的常用选项 选项 类型 说明 duration number 动画持续时间,单位为毫秒,默认值为400毫秒 timing…...

【java】@Transactional导致@DS注解切换数据源失效
最近业务中出现了多商户多租户的逻辑,所以需要分库,项目框架使用了mybatisplus所以我们自然而然的选择了同是baomidou开发的dynamic.datasource来实现多数据源的切换。在使用初期程序运行都很好,但之后发现在调用com.baomidou.mybatisplus.ex…...
003 SpringBoot集成Kafka操作
4.SpringBoot集成Kafka 文章目录 4.SpringBoot集成Kafka1.入门示例2.yml完整配置3.关键配置注释说明1. 生产者优化参数2. 消费者可靠性配置3. 监听器高级特性4. 安全认证配置 4.配置验证方法5.不同场景配置模板场景1:高吞吐日志收集场景2:金融级事务消息…...

Android SystemUI开发(一)
frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUI.java frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIService.java 关键文件 SystemUI 关键服务 简介 Dependency.class:处理系统依赖关系,提供资源或服…...

C#贪心算法
贪心算法:生活与代码中的 “最优选择大师” 在生活里,我们常常面临各种选择,都希望能做出最有利的决策。比如在超市大促销时,面对琳琅满目的商品,你总想用有限的预算买到价值最高的东西。贪心算法,就像是一…...

Vue程序下载
Vue是一个基于JavaScript(JS)实现的框架,想要使用它,就得先拿到Vue的js文件 Vue官网 Vue2:Vue.js Vue3:Vue.js - 渐进式 JavaScript 框架 | Vue.js 下载并安装vue.js 第一步:打开Vue2官网&a…...

【UCB CS 61B SP24】Lecture 17 - Data Structures 3: B-Trees学习笔记
本文以 2-3-4 树详细讲解了 B 树的概念,逐步分析其操作,并用 Java 实现了标准的 B 树。 1. 2-3 & 2-3-4 Trees 上一节课中讲到的二叉搜索树当数据是随机顺序插入的时候能够使得树变得比较茂密,如下图右侧所示,时间复杂度也就…...

机器学习决策树
一、香农公式 熵: 信息增益: 信息增益信息熵-条件熵 前者是初始信息熵大小,后者是因为条件加入后带来的确定性增加 信息增益表示得知特征X的信息而使得类Y的信息的不确定性减少的程度 信息增益越大说明影响越大 二、代码 ""&…...
Spring Boot + MyBatis 实现 RESTful API 的完整流程
后端开发:Spring Boot 快速开发实战 引言 在现代后端开发中,Spring Boot 因其轻量级、快速开发的特性而备受开发者青睐。本文将带你从零开始,使用 Spring Boot MyBatis 实现一个完整的 RESTful API,并深入探讨如何优雅地处理异…...
通过 ANSYS Discovery 进行 CFD 分析,增强工程设计
概括 工程师使用计算流体动力学 (CFD) 分析来研究和优化各种应用中的流体流动和传热分析。ANSYS Discovery 是一个用户友好的软件平台,使工程师能够轻松设置和解决 CFD 模型,并能够通知设计修改 在这篇博文中,我们将重点介绍在 Ansys Disc…...

家用可燃气体探测器——家庭燃气安全的坚实防线
随着社会的发展和变迁,天然气为我们的生活带来了诸多便利,无论是烹饪美食,还是温暖取暖,都离不开它的支持。然而,燃气安全隐患如影随形,一旦发生泄漏,可能引发爆炸、火灾等严重事故,…...

ListControl双击实现可编辑
为Edit Control控件添加丢失输入焦点事件,可见设为false 为List Control控件添加双击事件 控件和成员变量之间交换数据 CListCtrl ListPrint1; //列表输出 CEdit...

ave-form.vue 组件中 如何将产品名称发送给后端 ?
如何将产品名称发送给后端。 在这段代码中,产品名称(productName)的处理和发送主要发生在 save() 方法中。让我逐步分析: 产品ID的选择: <w-form-selectv-model"form.productId"label"涉及产品&q…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...

P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...

高危文件识别的常用算法:原理、应用与企业场景
高危文件识别的常用算法:原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件,如包含恶意代码、敏感数据或欺诈内容的文档,在企业协同办公环境中(如Teams、Google Workspace)尤为重要。结合大模型技术&…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...

让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...

智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...

C# 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...

回溯算法学习
一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...