一、点积(DOT)
本文主要介绍Dot函数在Shader中的使用方法、计算原理、实际的应用场景以及代码示例。同时也会对Dot函数做了一些延申。在Shader中,Dot函数主要用于计算两个向量的点积,它是一个非常常用的向量点乘函数,Dot可以计算两个向量之间的夹角、投影、投影比等等。掌握了Dot函数的使用方法和计算原理,可以为Shader的编写和优化提供很大的帮助。
1、数学原理
① 二维向量的点积
- 向量a和向量b的二维坐标:
- 点积结果如下:
② 三维向量的点积
- 向量a和向量b的三维坐标:
- 点积结果如下:
2、几何意义及功能
① 几何意义
向量的方向
点积结果等于两个向量的模长相乘再乘以它们夹角的余弦值。若两个向量都为单位向量,点积结果就是夹角的余弦值,可据此判断向量的方向关系,如结果为1,夹角为0度,方向相同;结果为-1,夹角为180度,方向相反;结果为0,夹角为90度,向量垂直。
阴影
点积还可用于计算一个向量在另一个向量方向上的投影长度,如在
方向上的投影长度为
。
长度
向量的长度|
| =
,可用于计算物体的速度大小、位移大小等物理量。
② 功能
从几何角度看:若两个向量都为单位向量,点积结果是两向量夹角的余弦值,据此可判断两向量的夹角情况 。如:
dot(normalize(vector1), normalize(vector2));
物理意义上:点积结果表示向量 vector2 在向量 vector1 方向上的投影长度,可用于计算力在某方向上的做功等。
3、语法
① 基本语法
其中 vector1 和 vector2 是两个要进行点积运算的同维度向量。
dot(vector1, vector2)
② 在GLSL中的示例
下边计算得到的 result 值为 1.0 * 3.0 + 2.0 * 4.0 = 11.0
vec2 a = vec2(1.0, 2.0);
vec2 b = vec2(3.0, 4.0); float result = dot(a, b);
4.、应用示例
① 计算光照强度
在漫反射光照模型中,通过计算表面法线向量与光照方向向量的点积,来确定光照对物体表面的影响程度。点积结果越大,光照强度越强。如:
half4 diff = albedo * _LightColor0 * max(0, dot(i.worldNormal, worldLight));
② 边缘检测
通过计算视线方向向量与法线方向向量的点积,可判断像素是否处于物体边缘。当夹角接近 度时,点积绝对值趋近于 ,透明度趋近于 ,实现边缘高亮效果。如
float newOpacity = min(1.0, tex.a / abs(dot(viewDirection, normalDirection)));
③ 积雪效果判断
计算面法线与雪落方向的点积,根据点积结果与积雪程度参数比较,判断是否有积雪。点积越大,积雪越多。
if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow)) ,
④ Fresnel效果
计算顶点或像素法线向量与相机向量的点积,得到一个随视角变化的渐变值,常用于实现物体边缘或中心提亮的效果,如衣物、物体及角色的高光效果等。
⑤ 向量长度计算
向量与自身做点积后再开方可得到向量长度,但有时可利用点积代替长度计算来优化性能,如计算相机到世界中某点的距离。
⑥ 伪光照效果
计算像素的世界位置与光源位置的点积,经处理后可模拟简单的光照衰减,实现伪光照效果。
⑦ 粒子效果圆形遮罩
在粒子效果中,通过计算纹理坐标与中心点坐标差值向量的点积,可得到具有球形渐变的圆形遮罩,用于限制粒子效果的显示范围,相比传统方法更高效。
⑧ 向量元素求和
计算向量与全为1的向量的点积,可快速得到向量元素的总和,如 dot(vector, 1) 。
5、代码示例
① Shader中用dot函数计算向量夹角处理向量的长度
在Shader中用 dot 函数计算向量夹角时,为避免向量长度影响夹角计算结果,需先将参与计算的向量归一化,以下是示例代码(基于GLSL):
// 顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec3 FragPos;
out vec3 Normal;
void main() {
// 将顶点位置转换到世界坐标
FragPos = vec3(model * vec4(aPos, 1.0));
// 转换法线向量到世界坐标并归一化
Normal = mat3(transpose(inverse(model))) * aNormal;
Normal = normalize(Normal);
// 输出裁剪空间的顶点位置
gl_Position = projection * view * vec4(FragPos, 1.0);
}
// 片段着色器
#version 330 core
out vec4 FragColor;
in vec3 FragPos;
in vec3 Normal;
uniform vec3 lightDir;
void main() {
// 归一化光照方向向量
vec3 lightDirNorm = normalize(lightDir);
// 用dot函数计算夹角余弦值
float cosTheta = dot(Normal, lightDirNorm);
// 通过反三角函数得到夹角
float angleInRadians = acos(cosTheta);
// 将弧度转换为角度
float angleInDegrees = radiansToDegrees(angleInRadians);
// 根据夹角设置颜色示例
FragColor = vec4(angleInDegrees/360.0, 0.0, 0.0, 1.0);
}
在上述代码中:
- 顶点着色器:将法线向量 aNormal 转换到世界坐标后,立即使用 normalize 函数将其归一化,这样传递给片段着色器的 Normal 向量长度为1。
- 片段着色器:在拿到光照方向向量 lightDir 后,同样用 normalize 函数将其归一化处理,变为单位向量。之后再进行 dot 函数计算,此时算出的 cosTheta 才精准反映两向量夹角的余弦值,不受原始向量长度干扰。
② Shader中用dot函数计算向量夹角
下面是一段使用GLSL编写的Shader代码,利用 dot 函数来计算向量夹角,实现简单的光照效果:
// 顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec3 FragPos;
out vec3 Normal;
void main() {
// 顶点位置变换到世界坐标
FragPos = vec3(model * vec4(aPos, 1.0));
// 法线向量变换到世界坐标并归一化
Normal = mat3(transpose(inverse(model))) * aNormal;
Normal = normalize(Normal);
// 计算顶点的裁剪空间位置
gl_Position = projection * view * vec4(FragPos, 1.0);
}
// 片段着色器
#version 330 core
out vec4 FragColor;
in vec3 FragPos;
in vec3 Normal;
uniform vec3 lightPos;
uniform vec3 lightColor;
void main() {
// 计算光照方向并归一化
vec3 lightDir = normalize(lightPos - FragPos);
// 使用dot函数计算夹角余弦值
float cosTheta = dot(Normal, lightDir);
// 获取夹角(弧度制)
float theta = acos(max(cosTheta, 0.0));
// 根据夹角计算光照强度
float intensity = max(0.0, cosTheta);
// 设置片段颜色
FragColor = vec4(lightColor * intensity, 1.0);
}
这段代码先在顶点着色器里把顶点位置和法线向量转换、归一化,传递到片段着色器。片段着色器计算光照方向向量,接着用 dot 函数算出它与法线向量夹角的余弦值,以此来确定光照强度,最终设置片段颜色。
6.、使用Dot函数的问题或限制
在使用Cg、HLSL、 GLSL(OpenGL Shading Language)等 shader 语言中的点积(dot)函数时,有以下潜在问题与限制。
① 向量维度匹配
- 问题: dot 函数用于计算两个向量的点积,传入的两个参数必须是维度相同的向量。比如在 GLSL 里,常见的是2D、3D 或 4D 向量,要是维度不一致,编译就会出错 。例如,将一个 vec2 和 vec3 作为参数传入 dot 函数,代码无法通过编译。
- 解决:编程前需梳理好数据,保证参与点积运算的向量维度相符,利用类型转换函数,把低维向量合理扩展,或者把高维向量恰当截断。
② 精度问题
- 问题:在一些对精度敏感的场景,浮点数精度损耗会影响结果。因为 shader 里的浮点数运算受限于硬件实现,尤其是在移动端 GPU 或老旧 GPU 上,反复的点积计算可能累积误差,微小的偏差不断累积,最终造成画面闪烁、渲染瑕疵。
- 解决:可以使用更高精度的数据类型,像 GLSL 中的 highp ,但这会占用更多资源,权衡性能与精度需求后谨慎使用;或是减少不必要的中间计算,降低精度损失累积。
③ 性能开销
- 问题:虽然点积计算本身效率较高,但要是在循环内频繁调用 dot 函数,或者处理海量顶点、片段时,计算开销不容小觑,导致帧率下降,渲染卡顿。
- 解决:尽可能把常量计算提取到循环外部,预先算好能复用的部分;优化算法逻辑,减少冗余的点积运算次数。
④ 语义理解偏差
- 问题:新手容易误解点积的几何、物理意义,错误运用点积结果。比如错把点积值直接当作距离使用,而实际上点积与向量夹角余弦相关,和距离并非同一概念。
- 解决:扎实掌握线性代数知识,清楚点积在光照计算、投影变换等场景下的正确用途,多参考成熟的 shader 代码案例,加深理解。
⑤ Dot函数的精度导致的画面闪烁问题
提高数据精度
- 使用更高精度数据类型:在 GLSL 等 shader 语言中,可将数据类型从默认精度改为 highp 等高精度类型,但要注意这可能会增加内存占用和计算开销,需根据实际情况权衡使用。
- 自定义精度计算:对于关键的中间计算结果,可通过额外的变量和计算步骤来提高精度。如先将向量分量乘以一个较大的常数,进行点积计算后再除以该常数恢复到合理范围,以减少精度损失。
优化计算逻辑
- 减少不必要运算:避免在循环内频繁调用 dot 函数,将能在循环外计算的部分提前计算好,减少精度损失累积。如多个顶点的光照计算中,可先计算出共用的光照方向等常量的相关值,再在循环内进行必要的计算。
- 重构算法:审视整个渲染算法和逻辑,看是否有更优的方式来实现相同效果且能减少 dot 函数的使用或降低其对精度的影响。比如使用近似算法或查找表等方式来替代部分复杂的点积计算。
调整渲染参数
- 增大近平面距离:在透视投影中,适当增大近平面距离,可使近平面附近物体的精度相对提高,减少因精度问题导致的闪烁,不过需注意这可能会影响场景的可视范围和物体的大小比例 。
- 调整深度缓冲区精度:根据具体的图形 API 和硬件平台,调整深度缓冲区的精度设置,以更好地适应场景中的深度变化,减少深度值相近的物体之间的闪烁现象 。
7.、Dot的优劣势
① 优势
- 计算简单高效,在图形处理单元(GPU)上可并行计算,能快速处理大量向量数据,提高渲染效率。
- 具有明确的几何和物理意义,便于理解和实现各种图形学和物理学相关的计算,如光照、投影等,可直观地模拟现实世界中的物理现象和视觉效果。
② 劣势
- 精度问题,在某些情况下,由于GPU的计算精度限制,点积结果可能存在精度误差,导致画面闪烁等问题,需采取提高数据精度等方法解决 。
- 单独的点积函数功能有限,通常需与其他数学函数和操作符结合使用,才能实现更复杂的计算和效果,如与normalize函数结合计算夹角余弦值。
8、Dot的适用范围和计算领域
① 计算领域
- 在图形学的光照计算中,通过计算表面法线向量与光源方向向量的点积,可得到漫反射光照强度。如:
fixed4 diff = albedo * _LightColor0 * max(0, dot(i.worldNormal, worldLight));
- 可用于计算向量投影,如将一个向量投影到另一个向量方向上,通过点积和向量长度计算投影向量。
- 在判断两个向量的相关性时,点积结果的正负和大小能反映向量的方向关系,如点积为0表示两向量垂直,大于0表示夹角小于90度,小于0表示夹角大于90度。
② 适用范围
- 广泛应用于各种图形渲染场景,如游戏开发中的角色、场景光照模型计算,以及实现阴影、高光等效果。
- 在计算机视觉领域,可用于图像特征提取、匹配等,通过计算图像像素点的向量表示之间的点积来判断特征相似性。
- 在数据可视化中,可根据数据的向量表示计算点积,以确定数据之间的某种关联程度,并据此进行可视化布局等。
二、延申
1、其它计算向量夹角的函数
除了 dot 函数,以下函数也可用于计算向量夹角:
① acos 函数
- 原理:已知两向量点积等于两向量模长乘积与夹角余弦值的乘积,即 dot(a, b) = |a| * |b| * cosθ ,由此可得 cosθ = dot(a, b) / (|a| * |b|) ,再通过 acos 函数可求出夹角 θ = acos(dot(a, b) / (|a| * |b|)) 。
- 适用场景:在图形学中,计算光照模型里表面法线向量与光源方向向量夹角等场景常用到,如 fixed4 diff = albedo * _LightColor0 * max(0, dot(i.worldNormal, worldLight)); 中,若要得到夹角,可使用 acos 函数。
② atan2 函数
- 原理: atan2 函数可根据两向量坐标计算出夹角的正切值,再通过反正切得到夹角,对于二维向量 a(x1,y1) 和 b(x2,y2) ,夹角 θ = atan2(y2-y1,x2-x1) - atan2(y1,x1) ,三维向量则需先将其投影到二维平面再计算。
- 适用场景:在一些需要精确角度计算且对角度范围有要求的场景中较有用,如在制作广告牌 shader 时,可用于计算广告牌面向方向与相机方向夹角等。
③ cross 叉乘函数
- 原理:两向量叉乘的模长等于两向量模长乘积与夹角正弦值的乘积,即 |a x b| = |a| * |b| * sinθ ,若已知两向量叉乘结果的模长及两向量模长,可通过 sinθ = |a x b| / (|a| * |b|) 求出夹角正弦值,再结合 asin 函数或其他方法求出夹角。
- 适用场景:常用于计算垂直于两向量平面的法向量,通过法向量与其他向量关系间接判断夹角,如判断物体表面某点是否在阴影中,可通过计算表面法线与光源方向叉乘得到的法向量,再与阴影投射方向比较夹角来确定。
④ Shader中用acos函数计算向量的夹角
以下是一个在Shader中使用 acos 函数计算向量夹角的具体案例,用于实现一个简单的光照效果,判断物体表面某点是否在光源的照射范围内:
// 顶点着色器部分
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec3 FragPos;
out vec3 Normal;
void main() {
// 将顶点位置转换到世界坐标
FragPos = vec3(model * vec4(aPos, 1.0));
// 将法线向量转换到世界坐标
Normal = mat3(transpose(inverse(model))) * aNormal;
// 计算顶点在裁剪空间中的位置
gl_Position = projection * view * vec4(FragPos, 1.0);
}
// 片段着色器部分
#version 330 core
out vec4 FragColor;
in vec3 FragPos;
in vec3 Normal;
uniform vec3 lightPos;
uniform vec3 lightColor;
uniform float lightAngle;
void main() {
// 计算表面法线与光照方向的单位向量
vec3 normal = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
// 计算向量夹角的余弦值
float cosTheta = dot(normal, lightDir);
// 使用acos函数得到夹角的弧度值
float angleInRadians = acos(cosTheta);
// 将弧度转换为角度
float angleInDegrees = radiansToDegrees(angleInRadians);
// 判断夹角是否在光源的照射角度范围内
if (angleInDegrees <= lightAngle) {
// 根据光照强度计算公式计算光照强度
float intensity = max(0.0, cosTheta);
FragColor = vec4(lightColor * intensity, 1.0);
} else {
FragColor = vec4(0.1, 0.1, 0.1, 1.0);
}
}
在上述代码中,首先在顶点着色器中把顶点位置和法线向量转换到世界坐标。然后在片段着色器中,计算表面法线向量与光照方向向量的夹角余弦值 cosTheta ,再通过 acos 函数得到夹角的弧度值 angleInRadians ,并将其转换为角度值 angleInDegrees 。最后,根据夹角与光源照射角度 lightAngle 的比较结果,计算光照强度并设置片段颜色,从而实现简单的光照效果模拟 。
⑤ atan2函数和dot函数区别
atan2 函数与 dot 函数主要存在以下区别:
计算结果类型
- dot函数:用于计算两个同维度向量的点积,返回的是一个标量。这个标量在几何意义上,若向量为单位向量,等同于两向量夹角的余弦值;物理意义上,是一个向量在另一个向量方向上的投影长度。例如, vec2 a = vec2(1.0, 0.0); vec2 b = vec2(0.0, 1.0); , dot(a, b) 返回 0 。
- atan2函数:接受两个浮点数作为参数,返回的是一个角度值(弧度制 ),用于确定从原点出发,到给定坐标点的向量与x轴正方向的夹角。如 atan2(1.0, 1.0) ,会得到 0.785398 弧度,即45° 。
输入参数
- dot函数:需要传入两个向量,并且这两个向量维度必须一致,常见的有二维向量 vec2 、三维向量 vec3 、四维向量 vec4 。如计算光照时,要传入表面法线向量和光源方向向量, dot(normalize(normal), normalize(lightDir)) 。
- atan2函数:接收两个标量,通常是一个点的y坐标和x坐标,常用来处理直角坐标转换为极坐标的场景, atan2(y, x) ,以此得到对应向量的角度。
应用场景
- dot函数:在图形学领域应用广泛,是光照计算的核心函数之一,通过点积判断光线与表面夹角来确定漫反射强度;也用于向量投影计算、判断向量间方向关系(夹角是锐角、直角还是钝角 )等。
- atan2函数:常用于涉及旋转角度计算的场景,比如在制作2D游戏中,计算角色朝向与目标方向的夹角;或是在一些纹理映射、坐标变换场景,精确得出向量与坐标轴夹角,辅助定位。
计算复杂度
- dot函数:计算较为简单直接,是向量对应分量相乘再相加,在GPU上可高效并行计算。
- atan2函数:涉及复杂的反正切运算,计算成本相比 dot 函数更高,使用频繁时会对性能产生更明显的影响。
2、GLSL中的dot函数
在Shader编程里,GLSL(OpenGL Shading Language) 是常用的着色语言,Shader中的dot函数其实通常就指GLSL中的dot函数,二者并没有本质区别。
①函数定义与功能
- GLSL的 dot 函数用于计算两个向量的点积。不管是顶点着色器、片元着色器,只要在GLSL代码里涉及向量运算,都能用该函数精准获取点积结果,运算规则遵循数学定义,二维向量、三维向量分别对应各自维度元素乘积之和。例如在一个简单的光照着色模型里,计算光线方向与平面法线方向的点积,以此衡量光照强度。
- 代码形式如下:
vec3 lightDir = normalize(lightPosition - fragPos);
vec3 normal = normalize(fragmentNormal);
float diff = max(dot(lightDir, normal), 0.0);
②使用场景
- 只要是基于OpenGL的图形渲染管线,用到GLSL编写Shader程序, dot 函数的使用场景都是相通的。从基础的光照计算,利用点积判断光线与表面夹角来分配光强;到复杂的几何变换,判断向量间的相对位置关系辅助变形计算,整个OpenGL生态下的Shader开发, dot 函数都是按照标准的向量点积运算逻辑服务于渲染需求。
③兼容性
- 不同版本的GLSL对 dot 函数的底层优化、调用效率或许稍有不同,但函数签名、输入输出要求和核心运算逻辑高度一致,确保老代码在新GLSL版本或者新显卡驱动下,涉及 dot 函数的部分能平稳过渡,正常执行向量点积运算。 所以总体而言,Shader范畴内提及的 dot 函数基本等同于GLSL里的对应函数。
3、不同编程语言和库中dot 函数的差异
① Python与NumPy
- 功能: numpy.dot 用于计算两个数组的点积。点积在数学上,对于两个向量而言,是对应元素相乘再求和;对于矩阵,它的运算规则基于线性代数的矩阵乘法。
- 示例:
import numpy as np
# 向量点积
a = np.array([1, 2])
b = np.array([3, 4])
print(np.dot(a, b))
# 输出 1*3 + 2*4 = 11
# 矩阵乘法
matrix_a = np.array([[1, 2], [3, 4]])
matrix_b = np.array([[5, 6], [7, 8]])
print(np.dot(matrix_a, matrix_b))
# 按矩阵乘法规则运算,输出对应二维数组
② PyTorch
- 功能:在PyTorch深度学习框架里, torch.dot 同样用于计算点积,不过它主要针对一维的张量(类似向量) 。
- 示例:
import torch
a = torch.tensor([1, 2])
b = torch.tensor([3, 4])
print(torch.dot(a, b))
# 输出 1*3 + 2*4 = 11
③ TensorFlow
- 在TensorFlow中,可利用 tf.tensordot 函数来实现类似效果,它更加通用,不仅能完成简单的向量点积、矩阵乘法,还能处理高维张量,通过指定合适的轴参数,灵活实现沿特定维度收缩张量的乘法运算,满足复杂深度学习和张量运算场景需求。
- 示例:
import tensorflow as tf
a = tf.constant([1, 2])
b = tf.constant([3, 4])
print(tf.tensordot(a, b, axes=1))
# 输出 11