Appearance
大海 -1
点击运行
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<canvas v-if="isRunning" id="sea1" class="shader-toy-stage bg-black"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick } from 'vue'
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onStart()
} else {
isRunning.value = false
}
}
const onStart = () => {
import('glslCanvas').then(module => {
const canvas = document.getElementById('sea1')
const glslCanvas: any = new module.default(canvas)
glslCanvas.load(`
#extension GL_OES_standard_derivatives: enable
precision highp float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
#define EPSILON_NRM (0.1 / u_resolution.x) // 是一个宏定义,用于定义一个常量
const int NUM_STEPS = 32;
const float PI = 3.1415926;
const float EPSILON = 1e-3; // 'epsilon'(ε)
// sea
const int ITERATION_GEOMETRY = 3; // 迭代几何
const int ITERATION_FRAGMENT = 5; // 迭代片段
const float SEA_HEIGHT = 0.6; // 海平面高度
const float SEA_CHOPPY = 4.0; // 海浪的波动
const float SEA_SPEED = 0.8; // 海浪的速度
const float SEA_FREQ = 0.16; // 海浪的频率
const vec3 SEA_BASE = vec3(0.0, 0.09, 0.18); // 海浪的基础颜色
const vec3 SEA_WATER_COLOR = vec3(0.8, 0.9, 0.6) * 0.6; // 海水的颜色
#define SEA_TIME (1.0 + u_time * SEA_SPEED) // 海浪的时间
const mat2 octave_m = mat2(1.6, 1.2, -1.2, 1.6); // 倍频程矩阵
// 这段代码将一个包含欧拉角的向量 vec3 ang 转换为一个旋转矩阵 mat3 m
// 欧拉角是三个角度,分别表示绕 x 轴、y 轴和 z 轴的旋转
// 这个函数的目的是根据这三个角度计算出相应的旋转矩阵
// 旋转矩阵的计算基于欧拉角的旋转顺序
// 通常,旋转顺序可以是任意的,但这段代码假设的旋转顺序是先绕 x 轴旋转,然后绕 y 轴旋转,最后绕 z 轴旋转
// 旋转矩阵的计算公式是:
// R=RzRyRx
// --------------------
// Rx=
// 1 0 0
// 0 cos(θx) −sin(θx)
// 0 sin(θx) cos(θx)
// --------------------
// Ry=
// cos(θy) 0 sin(θy)
// 0 1 0
// −sin(θy) 0 cos(θy)
// --------------------
// Rz=
// cos(θz) −sin(θz) 0
// sin(θz) cos(θz) 0
// 0 0 1
// --------------------
// RyRx=
// cos(θy) sin(θy)sin(θx) sin(θy)cos(θx)
// 0 cos(θx) −sin(θx)
// −sin(θy) cos(θy)sin(θx) cos(θy)cos(θx)
// --------------------
// Rz(RyRx)=
// cos(θz) −sin(θz) 0 cos(θy) sin(θy)sin(θx) sin(θy)cos(θx)
// sin(θz) cos(θz) 0 * 0 cos(θx) −sin(θx)
// 0 0 1 −sin(θy) cos(θy)sin(θx) cos(θy)cos(θx)
// 得到最终的 finalMatrix
// cos(θy)cos(θz) sin(θy)sin(θx)cos(θz)−cos(θx)sin(θz) sin(θy)cos(θx)cos(θz)+sin(θx)sin(θz)
// cos(θy)sin(θz) sin(θy)sin(θx)sin(θz)+cos(θx)cos(θz) sin(θy)cos(θx)sin(θz)−sin(θx)cos(θz)
// −sin(θy) cos(θy)sin(θx) cos(θy)cos(θx)
mat3 getRotationMatrixByEulerAngle(vec3 ang) {
vec2 a1 = vec2(sin(ang.x), cos(ang.x)); // 计算绕 x 轴旋转的角度 ang.x 的正弦和余弦值类似人头上下点头的动作
vec2 a2 = vec2(sin(ang.y), cos(ang.y)); // 计算绕 y 轴旋转的角度 ang.y 的正弦和余弦值类似人头左右摇头的动作
vec2 a3 = vec2(sin(ang.z), cos(ang.z)); // 计算绕 z 轴旋转的角度 ang.z 的正弦和余弦值类似人头左右摆头的动作
mat3 finalMatrix; // 定义一个 3x3 的矩阵
finalMatrix[0] = vec3(a1.y * a3.y + a1.x * a2.x * a3.x, a1.y * a2.x * a3.x + a3.y * a1.x, -a2.y * a3.x); // 计算旋转矩阵的第一行
finalMatrix[1] = vec3(-a2.y * a1.x, a1.y * a2.y, a2.x); // 计算旋转矩阵的第二行
finalMatrix[2] = vec3(a3.y * a1.x * a2.x + a1.y * a3.x, a1.x * a3.x -a1.y * a3.y * a2.x, a2.y * a3.y); // 计算旋转矩阵的第三行
return finalMatrix;
}
// sky
// 根据给定的方向向量 e 计算天空的颜色这个函数模拟了天空的渐变效果,从地平线到天顶的颜色变化
vec3 getSkyColor(vec3 e) {
// max(e.y, 0.0):确保 e.y 的值不小于 0,因为天空的颜色通常在地平线以上
// max(e.y, 0.0) * 0.8:将 e.y 的值乘以 0.8,使颜色变化更加平缓
// max(e.y, 0.0) * 0.8 + 0.2:在乘以 0.8 的基础上加上 0.2,确保地平线处的颜色不会太暗
// (max(e.y, 0.0) * 0.8 + 0.2) * 0.8:再次乘以 0.8,进一步调整颜色的亮度
e.y = (max(e.y, 0.0) * 0.8 + 0.2) * 0.8;
// pow(1.0 - e.y, 2.0):计算 1.0 - e.y 的平方,用于模拟天空颜色的非线性变化这个值将用于红色分量,使天空在地平线处更红
// 1.0 - e.y:直接使用 1.0 - e.y 作为绿色分量,使天空在地平线处更绿
// 0.6 + (1.0 - e.y) * 0.4:计算蓝色分量,使天空在地平线处更蓝 0.6 是基础蓝色,(1.0 - e.y) * 0.4 根据 e.y 的值增加蓝色
// vec3(pow(1.0 - e.y, 2.0), 1.0 - e.y, 0.6 + (1.0 - e.y) * 0.4):将计算得到的红、绿、蓝分量组合成一个颜色向量
// vec3(pow(1.0 - e.y, 2.0), 1.0 - e.y, 0.6 + (1.0 - e.y) * 0.4) * 1.1:将颜色向量乘以 1.1,增加颜色的亮度
return vec3(pow(1.0 - e.y, 2.0), 1.0 - e.y, 0.6 + (1.0 - e.y) * 0.4) * 1.1;
}
float hash(vec2 p) {
float h = dot(p,vec2(127.1, 311.7));
return fract(sin(h) * 43758.5453123);
}
float noise(in vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return -1.0 + 2.0 * mix(mix(hash(i + vec2(0.0, 0.0)),
hash(i + vec2(1.0, 0.0)), u.x),
mix(hash(i + vec2(0.0, 1.0)),
hash(i + vec2(1.0, 1.0)), u.x), u.y);
}
// 函数旨在计算漫反射光的强度
// 接受三个参数:法向量 n、光源方向 l 和一个粗糙度参数 p(有时也称为“漫反射指数”)
// 函数返回一个浮点数,代表漫反射光的强度
float diffuse(vec3 n, vec3 l, float p) {
// 首先,使用 dot 函数计算法向量 n 和光源方向 l 之间的夹角的余弦值
// 这个余弦值表示了光线与表面法线之间的对齐程度:当光线垂直于表面时,余弦值为 1;当光线与表面平行时,余弦值为 0(或负值,但在此上下文中只关心其绝对值,因为负值表示光线实际上是从表面后面照射过来的,这在物理上是没有意义的)
// 接下来,将这个余弦值乘以 0.4 并加上 0.6,这个操作为了调整漫反射光的强度分布(可要可不要)
// 乘以 0.4 会降低余弦值对最终强度的影响,而加上 0.6 则确保即使余弦值为 0(即光线与表面几乎平行)时,漫反射光也会有一定的基础强度,这种调整可能是基于特定的光照模型或艺术效果需求
// 然后,使用 pow 函数将这个调整后的值提升到 p 次方
// 这个操作通常用于模拟表面的粗糙度
// 当 p 值较大时,漫反射光的强度分布会更加集中在法线附近(即光线更垂直于表面时强度更高),模拟出更光滑的表面
// 而当 p 值较小时,漫反射光的强度分布会更加均匀,模拟出更粗糙的表面
return pow(dot(n, l) * 0.4 + 0.6, p);
}
// 函数旨在计算镜面反射光的强度
// 它接受四个参数:法向量 n、光源方向 l、观察方向 e 和一个光泽度参数 s(通常称为“镜面高光指数”或“粗糙度”的倒数)
// 函数返回一个浮点数,代表镜面反射光的强度
float specular(vec3 n, vec3 l, vec3 e, float s) {
float nrm = (s + 8.0) / (PI * 8.0);
// 首先,reflect(e, n) 函数计算光线从表面反射的方向
// 然后,使用 dot 函数计算这个反射方向与光源方向 l 之间的夹角的余弦值
// max 函数确保这个余弦值不会小于 0.0,因为余弦值在光线和表面法线之间的夹角大于 90 度时为负,而负值在物理上是没有意义的(因为这意味着光线实际上并没有照射到表面上)
// 接下来,使用 pow 函数将这个余弦值的 s 次方计算出来
// 这个操作增强了高光效果:当 s 值较大时,只有反射方向与光源方向几乎完全对齐时,结果才会显著(产生尖锐的高光);而当 s 值较小时,即使反射方向与光源方向不完全对齐,结果也会相对较大(产生柔和的高光)
return pow(max(dot(reflect(e, n), l), 0.0), s) * nrm;
}
// choppy 需要每次迭代都不一样,计算海浪的波浪起伏
float seaOctave(vec2 uv, float choppy) {
uv += noise(uv);
vec2 wave1 = 1.0 - abs(sin(uv)); // 计算海浪的波浪起伏
vec2 wave2 = abs(cos(uv)); // 计算海浪的波浪起伏
wave1 = mix(wave1, wave2, wave1); // 混合
return pow(1.0 - pow(wave1.x * wave1.y, 0.65), choppy);
}
// 该函数基于给定的位置返回海洋平面的高度
float computedSeaPlaneHeight(vec3 point, int iteration) {
float freq = SEA_FREQ;
float amplitude = SEA_HEIGHT;
float choppy = SEA_CHOPPY;
vec2 tempUv = point.xz;
tempUv.x *= 0.75;
float d = 0.0; // 每个海浪分量的高度差
float h = 0.0; // 海洋高度
// 由于 WebGL 1.0 的限制,最简单且通常最可行的解决方案是使用固定的迭代次数
// WebGL 2.0 则不用这样,可以用动态传入的 iteration
if(iteration == ITERATION_GEOMETRY) {
for (int j = 0; j < ITERATION_GEOMETRY; j += 1) {
d = seaOctave((tempUv + SEA_TIME) * freq, choppy); // 计算海浪分量的高度差
d += seaOctave((tempUv - SEA_TIME) * freq, choppy); // 计算海浪分量的高度差
h += d * amplitude; // 将当前海浪分量的高度差 d 乘以振幅 amp,然后累加到总高度 h 上
tempUv *= octave_m;
freq *= 1.9; // 频率
amplitude *= 0.22; // 振幅
choppy = mix(choppy, 1.0, 0.2); // 混合
}
}
// 由于 WebGL 1.0 的限制,最简单且通常最可行的解决方案是使用固定的迭代次数
// WebGL 2.0 则不用这样,可以用动态传入的 iteration
if(iteration == ITERATION_FRAGMENT) {
for (int j = 0; j < ITERATION_FRAGMENT; j += 1) {
d = seaOctave((tempUv + SEA_TIME) * freq, choppy); // 计算海浪分量的高度差
d += seaOctave((tempUv - SEA_TIME) * freq, choppy); // 计算海浪分量的高度差
h += d * amplitude; // 将当前海浪分量的高度差 d 乘以振幅 amp,然后累加到总高度 h 上
tempUv *= octave_m;
freq *= 1.9; // 频率
amplitude *= 0.22; // 振幅
choppy = mix(choppy, 1.0, 0.2); // 混合
}
}
// 返回输入位置 point 的 y 分量减去计算出的海浪高度 h 这给出了该点相对于海平面的高度
return point.y - h;
}
// 目的是计算给定空间位置 p 处的法向量 n
// 这个函数采用了数值微分的方法来估算法向量,这通常用于基于体素(voxel)、高度图(heightMap)或其他形式的三维数据集中
// eps 是一个小的浮点数,用于计算相邻位置之间的差异,从而估算法向量的方向
vec3 getNormal(vec3 p, float eps) {
vec3 n;
n.y = computedSeaPlaneHeight(p, ITERATION_FRAGMENT); // 计算位置 p 处的地形高度或密度,并将其存储在 n.y 中
// 为什么减去 n.y,因为通过计算偏移点的高度与原始点高度(n.y)的差值,可以得到该方向上的高度变化率,即斜率,查看(自定义效果 - 海浪 -1.png)
// computedSeaPlaneHeight(vec3(p.x + eps, p.y, p.z), ITERATION_FRAGMENT) 计算点 p+ϵi 处的高度
// 计算位置 p 在 x 方向偏移 eps 后的地形高度或密度,并将其与 n.y(即原始位置 p 的高度或密度)相减,以估算 x 方向的斜率
// n.x ≈ h(p+ϵi)−h(p) / ϵ
n.x = computedSeaPlaneHeight(vec3(p.x + eps, p.y, p.z), ITERATION_FRAGMENT) - n.y;
// 为什么减去 n.y,因为通过计算偏移点的高度与原始点高度(n.y)的差值,可以得到该方向上的高度变化率,即斜率,查看(自定义效果 - 海浪 -1.png)
// computedSeaPlaneHeight(vec3(p.x, p.y, p.z + eps), ITERATION_FRAGMENT) 计算点 p+ϵk 处的高度
// 计算位置 p 在 z 方向偏移 eps 后的地形高度或密度,并将其与 n.y(即原始位置 p 的高度或密度)相减,以估算 z 方向的斜率
// n.z ≈ h(p+ϵk)−h(p) / ϵ
n.z = computedSeaPlaneHeight(vec3(p.x, p.y, p.z + eps), ITERATION_FRAGMENT) - n.y;
// eps 是 dot(distanceVector, distanceVector) * EPSILON_NRM
// 简化处理:在某些情况下,开发者可能会为了简化计算而进行一些近似处理
// 特殊需求:可能在应用场景中,法线的 y 分量需要根据 ϵ 进行调整
// 通常应该将法线的 y 分量设置为 1,而不是 ϵ,即 n.y = 1.0;
n.y = eps;
return normalize(n);
}
// 函数旨在计算海洋表面某一点 p 的颜色,考虑到法向量 n、光源方向 l、观察方向 eye 和到观察者的距离 dist
vec3 getSeaColor(vec3 p, vec3 n, vec3 l, vec3 eye, vec3 dist) {
// 计算菲涅耳效应,即光线从一种介质进入另一种介质时反射和折射的比例
// 这里使用点积来计算法向量 n 和观察方向 -eye (因为观察方向通常指向观察者,而现在需要的是指向光源或表面的相反方向) 之间的夹角的余弦值,然后用 1.0 减去这个值得到菲涅耳因子
// clamp 函数确保这个值在 0.0 到 1.0 之间
float fresnel = clamp(1.0 - dot(n, -eye), 0.0, 1.0);
// 对菲涅耳因子进行三次方运算,以增强菲涅耳效应在高角度时的效果(即当观察方向几乎与表面平行时)
// 然后,使用 min 函数确保结果不会超过 0.5,这可能是为了限制反射光的强度
fresnel = min(fresnel * fresnel * fresnel, 0.5);
// 计算反射光颜色 reflect 函数计算光线从表面反射的方向,然后 getSkyColor 函数返回该方向上的天空颜色
vec3 reflected = getSkyColor(reflect(eye, n));
// 计算折射光颜色
// 这里 SEA_BASE 表示海水的基础颜色
// diffuse 函数根据法向量 n、光源方向 l 和一个粗糙度参数(这里是 80.0)返回漫反射光的强度
// 这个强度乘以 SEA_WATER_COLOR(海水的颜色)和一个缩放因子(0.12)来得到折射光的颜色部分
// 然后,将这个部分加到 SEA_BASE 上
vec3 refracted = SEA_BASE + diffuse(n, l, 80.0) * SEA_WATER_COLOR * 0.12;
// 使用 mix 函数根据菲涅耳因子将折射光和反射光的颜色混合在一起菲涅耳因子越高,反射光的成分就越多;反之,折射光的成分就越多
vec3 color = mix(refracted, reflected, fresnel);
// 计算衰减因子
// 这里使用 dist 向量与其自身的点积(即 dist 的长度的平方)乘以一个衰减系数(0.001)
// 然后从 1.0 中减去这个结果
// max 函数确保结果不会小于 0.0
// 这个衰减因子用于模拟随着距离增加海水颜色逐渐减弱的效果
float attenuation = max(1.0 - dot(dist, dist) * 0.001, 0.0);
// 根据点 p 的 y 坐标与海面高度 SEA_HEIGHT 的差异来调整颜色
// 这个差异乘以一个缩放因子(0.18)和之前计算的衰减因子 atten
// 然后乘以 SEA_WATER_COLOR 并加到最终颜色上
// 这可能用于模拟海水深浅不同时的颜色变化
color += SEA_WATER_COLOR * (p.y - SEA_HEIGHT) * 0.18 * attenuation;
// 计算并添加镜面反射光颜色
// specular 函数返回镜面反射光的颜色
// 这个颜色被直接加到最终颜色上
color += specular(n, l, eye, 60.0);
return color;
}
// 高度图的描图
// 作用是进行光线与高度图(如海面)的交点追踪,用于在给定起点和方向的情况下,找到与海面相交的位置
// 具体来说,通过一系列的迭代计算,确定光线从起点 rayOrigin 沿着方向 direction 出发,与高度图(在这个场景中是海面)相交的具体位置,并将这个交点位置输出到参数 intersectionPoint 中
// 这个函数是渲染海洋场景的关键部分,它结合了深度映射(depth mapping)和步进(stepping)技术来确定光线与海洋表面的交点
float heightMapTracing(vec3 rayOrigin, vec3 direction, out vec3 intersectionPoint) {
float nearPlane = 0.0; // 初始化近平面(near plane)的距离为 0
float farPlane = 1000.0; // 初始化远平面(far plane)的距离 farPlane 为一个较大的数(1000.0),这是光线投射的最大距离
// 先计算 farHeight,再计算 nearHeight 的原因
// 在渲染或图形处理中,远处的物体通常对最终图像的影响较小,因此可以先以较低的精度或分辨率计算远处的高度(hx),以减少计算量
// 随后,再对近处的物体进行更精细的计算(hm),以确保图像质量
// 所以会 return farPlane
float farHeight = computedSeaPlaneHeight(rayOrigin + direction * farPlane, ITERATION_GEOMETRY); // 计算远平面位置的高度
// 如果 farHeight 小于 0,则表明了远平面位置的高度小于 0,即光线投射方向上没有海面的交点,这时直接返回 0.0,表示没有交点
// 需要计算近平面位置的高度,以确定光线投射方向上的海面交点位置
if(farHeight > 0.0) {
intersectionPoint = rayOrigin + direction * farPlane; // 如果远平面位置的高度大于 0,则将远平面位置作为交点位置
return farPlane; // 返回远平面的距离
}
// for 循环用于迭代计算光线投射方向上的海面交点位置,光线步进算法
float nearHeight = computedSeaPlaneHeight(rayOrigin + direction * nearPlane, ITERATION_GEOMETRY); // 计算近平面位置的高度
for (int i = 0; i < NUM_STEPS; i++) { // 开始一个循环,循环次数由 NUM_STEPS 定义,这是光线投射过程中的步进次数
// 计算当前步的中点距离 middle distance 这是通过线性插值(mix)nearPlane 和 farPlane 得到的,插值因子基于当前步的高度差与总高度差的比例
float tempDistance = mix(nearPlane, farPlane, nearHeight / (nearHeight - farHeight)); // 注意,此时的 farHeight 永远是小于 0 的
intersectionPoint = rayOrigin + direction * tempDistance;
float tempHeight = computedSeaPlaneHeight(rayOrigin + direction * tempDistance, ITERATION_GEOMETRY);
// 如果 tempHeight < 0.0,这意味着中点位置 p 的高度低于基准面,即光线投射方向上没有海面的交点,这时需要继续迭代步进,以确定交点位置,因此,更新远平面的时间(或距离)和高度
// 由于近平面的高度值 nearHeight 通常是正数(因为近平面被设置为稍高于地形表面),所以交点不可能位于近平面和中点之间
// 因此,可以推断出交点必定位于当前步的中点到远平面之间的某个位置
// 为了缩小搜索范围,算法将远平面的时间(或距离)farPlane 和高度值 farHeight 更新为中点的时间 tempDistance 和高度 tempHeight
// 因为所有比此刻的 tempDistance 更大的值,求出来的 tempHeight 也是小于 0 的,所以要把 farPlane 的值,变小
if(tempHeight < 0.0) {
farPlane = tempDistance;
farHeight = tempHeight;
} else {
nearPlane = tempDistance;
nearHeight = tempHeight;
}
if(abs(tempHeight) < EPSILON) break;
}
// 循环结束后,通过线性插值返回最终的交点距离这个插值是基于最终步的高度差与总高度差的比例来计算的
return mix(nearPlane, farPlane, nearHeight / (nearHeight - farHeight));
}
vec3 getPixel(in vec2 uv, float time) {
// ray 的内容---start
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// 这一行定义了一个三维向量 ang,表示欧拉角(Euler angles),用于描述光线方向的旋转
vec3 angle = vec3(sin(1.25 * time) * 0.1, sin(time) * 0.2 + 0.3, time * 0.5);
// 表示从观察点(通常是相机位置)到屏幕上的某个像素点的方向向量这个向量用于光线追踪,确定光线从相机出发,穿过屏幕上的某个像素点,进入场景的方向
// 通过归一化操作,将从相机位置到屏幕上的某个像素点的方向向量转换为一个单位向量
// 这个方向向量用于光线追踪,确定光线在场景中的传播路径,确保计算结果的准确性和高效性
vec3 direction = normalize(vec3(uv.xy, -2.0));
// 这一行的作用是对光线的方向向量 dir 的 z 分量进行调整具体来说,通过增加一个与 uv 向量长度成比例的值来改变光线的倾斜角度这种调整可以模拟出从不同视角观察场景时,光线方向的变化,从而增强场景的立体感和深度感
direction.z += length(uv) * 0.14;
// 这一行的作用是将光线的方向向量 dir 进行归一化,保持方向一致性,并根据欧拉角 ang 对光线方向进行旋转
direction = normalize(direction) * getRotationMatrixByEulerAngle(angle);
// rayOrigin 表示光线的起源点,即射线的起点
// 作用:
// 1、光线追踪的起点:在光线追踪算法中,从 rayOrigin 这个点出发,沿着 direction(光线的方向向量)进行追踪,与场景中的物体(如海面)进行交点计算
// 2、影响观察视角和场景动态:rayOrigin 的 z 坐标随时间变化,使得光线的起点在 z 轴方向上随时间移动,从而产生动态的观察效果,增加了场景的真实感和动态感同时,其 y 坐标值决定了观察视角的高度,影响观察者对场景的视野范围和细节感知
vec3 rayOrigin = vec3(0.0, 3.5, 5.0);
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// ray 的内容---end
// tracing 的内容---start
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// 相交点
vec3 intersectionPoint;
// distanceFromStart2End 表示从起点到相交点的距离
float distanceFromStart2End = heightMapTracing(rayOrigin, direction, intersectionPoint);
vec3 distanceVector = intersectionPoint - rayOrigin; // 计算相交点与起点的距离
vec3 n = getNormal(intersectionPoint, dot(distanceVector, distanceVector) * EPSILON_NRM);
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// tracing 的内容---end
// light
// light 表示光源的方向
// normalize(vec3(0.0, 1.0, 0.8)) 得到的结果是一个单位向量,大约为 vec3(0.0, 0.7810, 0.6248)
// 这个向量的方向与原始向量相同,但长度为 1,常用于表示方向,特别是在光照计算中作为光源方向向量
// 作用:
// 1、光照计算的依据:在光照模型中,light 用于计算光线与物体表面的交互效果,如漫反射和镜面反射等它指定了光源相对于场景中物体的方向,从而影响物体表面的明暗、颜色和高光等视觉效果
// 2、影响场景的光照效果:通过与物体表面的法线向量等进行点乘等运算,计算出不同光照成分(如漫反射光、镜面反射光等)的强度,进而影响最终渲染出的图像的光影效果,使场景具有立体感和真实感
vec3 light = normalize(vec3(0.0, 1.0, 0.8));
return mix(
getSkyColor(direction),
getSeaColor(intersectionPoint, n, light, direction, distanceVector),
pow(smoothstep(0.0, -0.02, direction.y), 0.2));
}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
vec2 uv = (fragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);
uv *= 3.0;
float time = u_time * 0.3 + u_mouse.x * 0.01;
vec3 color = getPixel(uv, time);
gl_FragColor = vec4(pow(color, vec3(0.65)), 1.0);
}`)
})
}
</script>
火 -1
为什么是-u_time,而不是 u_time,具体解释看 fire2 的 main 函数里面的解析
点击运行
点击展开文字
<template>
<div>
<div>为什么是-u_time,而不是 u_time,具体解释看 fire2 的 main 函数里面的解析</div>
<div class="flex space-between">
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<div @click="showText = !showText" class="pointer">点击{{ !showText ? '展开': '收起'}}文字</div>
</div>
<div v-if="showText">
<p>1、为什么选择椭球体作为火焰的基础形状?</p>
<p> 1.1---火焰的形状通常是一个向上延伸的、逐渐变细的形状。椭球体(或扁平的椭球体)是一个很好的基础形状,因为它可以近似火焰的整体轮廓。</p>
<p> 1.2---通过调整椭球体的参数(如扁平程度),可以更好地控制火焰的形状。</p>
<p>----------------------------------------</p>
<p>2、为什么使用光线行进算法(Ray Marching)?</p>
<p> 2.1---光线行进算法是一种用于渲染复杂场景(如体积效果、SDF 等)的算法。它通过沿着光线方向逐步前进,计算光线与场景的交点。</p>
<p> 2.2---这种方法非常适合渲染火焰,因为它可以处理火焰的不规则形状和动态变化。</p>
</div>
<canvas v-if="isRunning" id="fire1" class="shader-toy-stage bg-black"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted } from 'vue'
const isRunning = ref(false)
const showText = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onStart()
} else {
isRunning.value = false
}
}
onMounted(async () => {
await nextTick()
})
// scene(p) === min(100.0 - length(p), abs(fireDist))
// flame(p) === fireDist
// -------------------------------------------
// 1. 场景距离(scene(p))
// scene(p) 是一个全局的距离场函数,它计算点 p 到整个场景中所有物体的最近距离。这个场景可能包含多个物体,例如火焰、边界球体、地面等
// 具体作用
// 全局距离计算:scene(p) 返回的是点 p 到场景中所有物体的最近距离。它是一个综合的距离场,用于判断点 p 是否接近或到达场景中的任何物体
// 光线行进的终止条件:在光线行进算法中,当 scene(p) <= eps 时,认为光线已经到达或穿过场景的表面,此时应该停止行进。eps 是一个小的阈值,用于避免数值误差
// 2. 火焰距离(flame(p))
// flame(p) 是一个局部的距离场函数,它专门计算点 p 到火焰形状的最近距离。这个函数只关注火焰的形状,不考虑其他物体
// 具体作用
// 局部距离计算:flame(p) 返回的是点 p 到火焰形状的最近距离。它是一个局部的距离场,用于判断点 p 是否在火焰内部、表面或外部
// 火焰效果的模拟:通过 flame(p),可以判断点 p 是否在火焰内部,从而模拟火焰的发光效果。如果 flame(p) < 0,说明点 p 在火焰内部;如果 flame(p) = 0,说明点 p 在火焰表面;如果 flame(p) > 0,说明点 p 在火焰外部
// 3. 场景距离和火焰距离的区别
// 范围:
// scene(p):全局范围,考虑场景中的所有物体
// flame(p):局部范围,只考虑火焰形状
// 用途:
// scene(p):用于控制光线行进的终止条件,确保光线不会穿透场景表面
// flame(p):用于模拟火焰的发光效果,判断点 p 是否在火焰内部
// 4. 具体例子
// 假设场景中包含以下物体:
// 一个火焰形状,由 flame(p) 定义
// 一个边界球体,用于限制光线的行进范围
// 一个地面平面
// scene(p) 的计算
// scene(p) 会综合考虑这些物体,返回点 p 到这些物体的最近距离。例如:
// float scene(vec3 p) {
// float d1 = sphere(p, vec4(0., 0., 0., 10.)); // 边界球体
// float d2 = flame(p); // 火焰形状
// float d3 = plane(p, vec4(0., 1., 0., -5.)); // 地面平面
// return min(min(d1, d2), d3); // 返回最近距离
// }
// 在这个例子中,scene(p) 返回的是点 p 到边界球体、火焰形状和地面平面的最近距离
// flame(p) 的计算
// flame(p) 只考虑火焰形状,返回点 p 到火焰的最近距离。例如:
// float flame(vec3 p) {
// float d = sphere(p * vec3(1., 0.5, 1.), vec4(0., -1., 0., 1.));
// return d + (noise(p + vec3(0., iTime * 2., 0.)) + noise(p * 3.) * 0.5) * 0.25 * p.y;
// }
// 在这个例子中,flame(p) 返回的是点 p 到火焰形状的最近距离,并结合噪声函数模拟火焰的动态效果
// 5. 为什么需要两个距离
// scene(p):确保光线在场景中正确行进,不会穿透任何物体表面。它是一个全局的距离场,用于控制光线行进的终止条件
// flame(p):专门用于模拟火焰的发光效果。它是一个局部的距离场,用于判断点 p 是否在火焰内部
// 6. 总结
// scene(p) 是全局的距离场,用于控制光线行进的终止条件,确保光线不会穿透场景表面
// flame(p) 是局部的距离场,用于模拟火焰的发光效果,判断点 p 是否在火焰内部
// 通过这两个距离的计算,可以确保光线在场景中正确行进,并在火焰区域产生正确的发光效果
// -------------------------------------------
// fireDist 和 min(100.0 - length(p), abs(fireDist)) 都是计算距离场
// 场景表面:由 min(100.0 - length(p), abs(fireDist)) 定义,表示点 p 到整个场景的最近距离为零的集合,场景可能包含多个物体(如火焰、边界等),用于控制光线行进的终止条件
// 火焰表面:由 fireDist 定义,表示点 p 到火焰形状的最近距离为零的集合,用于模拟火焰的发光效果
// -------------------------------------------
// 为什么在未到达场景表面时(d > eps)判断是否在火焰内部?
// 关键在于距离场的性质:
// min(100.0 - length(p), abs(fireDist)) 返回的是点 p 到整个场景的最小距离。如果 > 0,说明点 p 在场景的外部;如果 = 0,说明点 p 在场景的表面上;如果 < 0,说明点 p 在场景的内部
// fireDist 返回的是点 p 到火焰形状的最小距离。如果 > 0,说明点 p 在火焰的外部;如果 = 0,说明点 p 在火焰的表面上;如果 < 0,说明点 p 在火焰的内部
// -------------------------------------------
// 为什么只能在 d>eps 才能继续判断 fireDist < .0,如果 d<eps 时候判断 fireDist < .0,会怎么样?
// 在光线行进算法中,d 表示当前点 p 到整个场景的最近距离。如果 d > eps,说明当前点 p 还在场景的外部,光线可以继续向前行进。如果 d <= eps,则认为光线已经到达或穿过场景的表面,此时应该停止行进。
// 如果在 d < eps 时仍然判断 fireDist < .0,可能会导致以下问题:
// a. 光线穿透表面
// 如果 d < eps,说明点 p 已经非常接近或已经到达场景的表面。此时,如果 fireDist < 0,说明点 p 在火焰内部,但光线可能已经穿过了火焰表面。这会导致光线在火焰内部行进,而不是在表面或外部。
// b. 错误的发光效果
// 在 d < eps 时,如果 fireDist < 0,可能会错误地计算发光强度 glow。因为此时点 p 已经非常接近或已经到达场景的表面,继续计算发光强度可能会导致不正确的火焰效果。
// c. 光线行进终止
// 在光线行进算法中,当 d < eps 时,通常认为光线已经到达场景的表面,应该停止行进。如果此时仍然判断 fireDist < 0,可能会导致光线在火焰内部行进,而不是在表面或外部,从而影响最终的渲染结果。
// -------------------------------------------
// 正确的逻辑
// 正确的逻辑是在 d > eps 时判断 fireDist < .0,这样可以确保:
// 光线在场景的外部行进
// 当光线进入火焰区域时,可以正确地计算发光强度 glow
// 当光线到达场景的表面时,可以正确地终止行进
const onStart = () => {
import('glslCanvas').then(module => {
const canvas = document.getElementById('fire1')
const glslCanvas: any = new module.default(canvas)
glslCanvas.load(`
#extension GL_OES_standard_derivatives: enable
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
// 左手坐标系
// ro:相机的位置(Ray Origin),即相机在三维空间中的坐标这个点是所有视线(或光线)的起点
// target:相机的目标点,即相机“看”向的点这个点决定了相机的前进方向(Forward 向量)
// up:相机的向上方向(Up 向量),通常与相机的前进方向垂直,这个向量用于确定相机的右侧方向(Right 向量)和确保相机的坐标系是正交的
// 相机源点、目标、向上方向
// R、U、F 分别是 Right、Up 和 Forward 向量
mat3 getCameraMat(vec3 ro, vec3 target, vec3 up) {
vec3 f = normalize(target - ro); // 计算 Forward 向量(F)
// 叉积 cross(a, b) 的结果是一个垂直于向量 a 和 b 的向量
// 注意:由于使用的是左手坐标系,所以是使用 up 叉乘 f,而不是反过来进行叉乘,进行叉乘运算时一定要注意其方向性!
vec3 r = cross(up, f); // Right 向量(R)是 Forward 向量和 Up 向量的叉积,表示相机的右侧方向
vec3 u = normalize(cross(f, r)); // 为了确保 Up 向量垂直于 Forward 向量,需要重新计算 Up 向量为 Right 向量和 Forward 向量的叉积
return mat3(r, u, f);
}
// 基于 Perlin 噪声的火焰效果噪声函数
// 作用:这是一个 3D 噪声函数,用于生成平滑的随机值
// 由于 Perlin 噪声的性质,这个函数的输出值在 [−1,1] 范围内
float noise(vec3 p) {
vec3 i = floor(p);
// a 是一个四维向量,它的每个分量都是 i 与向量 (1.0,57.0,21.0) 的点积,再加上一个常数向量 (0.0,57.0,21.0,78.0)
vec4 a = dot(i, vec3(1.0, 57.0, 21.0)) + vec4(0.0, 57.0, 21.0, 78.0);
// f 是一个三维向量,它的每个分量都是 (p−i) 乘以 π(即 acos(-1.0)),然后取余弦值,再进行线性变换得到的。这个变换将 (p−i) 的范围从 [0,1] 映射到 [−0.5,0.5],然后平移到 [0,1]
vec3 f = cos((p - i) * acos(-1.0)) * (-0.5) + 0.5;
// d 是一个四维向量,它的每个分量都是通过在两个正弦值之间进行插值得到的。这两个正弦值分别是 sin(cos(a)∗a) 和 sin(cos(1.0+a)∗(1.0+a)),插值因子是 f.x
vec4 d = mix(sin(cos(a) * a), sin(cos(1.0 + a) * (1.0 + a)), f.x);
// d 的前两个分量被更新为在 (d.x,d.z) 和 (d.y,d.w) 之间进行插值得到的值,插值因子是 f.y
d.xy = mix(d.xz, d.yw, f.y);
// 最后,函数返回 d.x 和 d.y 之间的插值,插值因子是 f.z
return mix(d.x, d.y, f.z);
}
// 球体的定义
// spr.xyz 是球心的坐标,spr.w 是球的半径
float getSphereDist(vec3 p) {
vec4 sphere = vec4(0.0, 0.0, 0.0, 1.0); // 把第二个设成 -1.0 可以看到下半部,把第二个设成 1.0 看到上半部,把第二个设成 0.0 看到全部
vec3 sphereVec = vec3(1.0, 1.0, 1.0);
vec3 ellipseVec = vec3(1.0, 0.5, 1.0);
return length(sphere.xyz - p * sphereVec) - sphere.w;
}
// 椭圆的定义:
// 椭圆可以看作是一个被拉伸或压缩的圆
// 如果在某个轴上压缩点的坐标,那么在该轴方向上,椭圆会看起来被拉伸
// ---------------------------------------------
// 椭圆的公式
// ((x - h) * (x - h)) / (a * a) + ((y - k) * (y - k)) / (b * b) + ((z - m) * (z - m)) / (c * c) = 1
// ---------------------------------------------
// (x * x) / (1 * 1) + (y * y) / (0.5 * 0.5) + (z * z) / (1 * 1) = 1
// x^2 + 4y^2 = 1
// 由于 x 的系数是 1,y 的系数是 4,可以知道椭圆的长轴在 y 轴上,短轴在 x 轴上
float getEllipseDist(vec3 p) {
// point 进行非均匀的缩放,x 轴保持原比例,y 轴压缩为原来的 0.5 倍,z 轴保持原比例
// 由于是 y 轴被压缩,椭圆在 y 轴被被拉伸
vec3 pointScale = vec3(1.0, 0.5, 1.0);
// 平移
vec3 pointTranslate = vec3(0.0, 0.0, 0.0);
// 减去 1.0 是为了创建一个包含椭圆形状的隐式函数,定义了一个单位椭圆
float dist = length(p * pointScale - pointTranslate) - 1.0;
return dist;
}
// 太奇怪,思考不明白
// 正常而言,p 是随时间而向上移动,即 p + vec3(0.0, u_time * 3.0, 0.0) 。但是要-u_time 才能正确显示效果,即等于 p - vec3(0.0, u_time * 3.0, 0.0)
// noise(p * 3.0) * 0.5:添加更高频率的噪声,用于增加火焰的细节
// 0.25 * p.y:火焰的动态效果在 Y 轴方向上逐渐减弱,模拟火焰向上延伸时逐渐变细的特性
float fireNoise(vec3 p) {
// float n = noise(vec3(0.0, p.y + u_time, 0.0)); // 随着 u_time 的增大,是往下运动的
// return n;
// return p.y + u_time; // 随着 u_time 的增大,是往下运动的
// return u_time; // 随着 u_time 的增大,椭圆是逐渐变小
float n = noise(p + vec3(0.0, -u_time * 3.0, 0.0) + noise(p * 3.0) * 0.5) * 0.5 * p.y;
return n;
}
// rayOrigin 代表光线的起点
// rayDirection 代表光线的方向
vec4 rayMarching(vec3 rayOrigin, vec3 rayDirection) {
float glow = 0.0;
float eps = 0.02;
vec3 p = rayOrigin;
const float MAX_STEPS = 64.0;
for(float i = 0.0; i < MAX_STEPS; i += 1.0) {
float n = fireNoise(p);
float dist = getEllipseDist(p);
// float dist = getSphereDist(p);
// fireDist:计算点 p 到火焰形状的最近距离,调整火焰的形状和边界
// 目的是将椭圆形状(dist)与噪声(n)结合起来,从而生成一个动态的、不规则的火焰形状
float fireDist = dist + n;
// 100.0 - length(p)
// 这个表达式计算点 p 到场景边界的距离
// 假设场景是一个半径为 100 的球体,那么:
// length(p):计算点 p 到原点的距离
// 100.0 - length(p):计算点 p 到场景边界的距离
// 如果 length(p) < 100.0,则点 p 在场景内部,距离为正
// 如果 length(p) = 100.0,则点 p 在场景边界上,距离为零
// 如果 length(p) > 100.0,则点 p 在场景外部,距离为负
//-----------------------------------------------------------
// abs(fireDist)
// 这个表达式计算点 p 到火焰表面的绝对距离
// 火焰距离场 fireDist 可能是正数(点在火焰外部)、零(点在火焰表面)或负数(点在火焰内部)
// 取绝对值 abs(fireDist) 可以得到点 p 到火焰表面的最近距离
//-----------------------------------------------------------
// min(100.0 - length(p), abs(fireDist))
// 这个表达式取 100.0 - length(p) 和 abs(fireDist) 中的最小值,用于确定点 p 到场景中最近表面的距离
// 这可以确保:
// 如果点 p 更接近场景边界,使用 100.0 - length(p) 作为距离
// 如果点 p 更接近火焰表面,使用 abs(fireDist) 作为距离
//-----------------------------------------------------------
// + eps:确保 d 的值避免数值不稳定性或除零错误
//-----------------------------------------------------------
// 为什么不是 abs(fireDist) + eps?
// 如果只使用 abs(fireDist) + eps,则只考虑了点 p 到火焰表面的距离,而没有考虑点 p 到场景边界的距离。这可能导致以下问题:
// 当点 p 接近场景边界但远离火焰表面时,abs(fireDist) 可能很大,导致光线继续行进,穿透场景边界
// 当点 p 在火焰内部但接近场景边界时,abs(fireDist) 为正,但光线可能已经到达场景边界,应该停止行进
float d = min(100.0 - length(p), abs(fireDist)) + eps;
p += d * rayDirection;
// 如果 d > eps,说明当前点距离表面还有一段距离,光线可以继续前进
// 如果 d <= eps,说明光线已经接近表面,可以停止前进
// 这个条件用于避免光线在表面附近发生数值不稳定或无限循环的情况
// 表明 min(100.0 - length(p), abs(fireDist)) 取的是 abs(fireDist) 的值
if (d > eps) {
// 检查当前点是否在发光源的范围内(通过 fireDist < 0.0 判断,在内部)
// 如果在发光源范围内,标记当前点为被照亮(glowed = true)
// 如果当前点被照亮,则计算辉光强度(glow),其值随着迭代步数的增加而增加,但被归一化到 [0, 1) 的范围内
// 当光线在火焰内部时:glow 会逐渐增加,表示光线越接近火焰表面,发光强度越高
// 当光线在火焰外部时:glow 保持为 0,表示没有发光
// 当光线接近火焰边界时:glow 的值会接近 1,表示发光强度最大
if (fireDist < 0.0) {
glow = i / MAX_STEPS;
}
}
}
return vec4(p, glow);
}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
vec2 uv = 2.0 * (fragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x); // 归一化 uv 的坐标,范围在 [-1, 1] 之间
// vec2 uv = fragCoord.xy / u_resolution.xy; // 将屏幕坐标归一化到 [0, 1] 范围内
uv.x *= u_resolution.x / u_resolution.y; // 修正宽高比,使得火焰变细或变粗
// 方式一:
// rayDirection 是光线的方向向量,表示光线沿着哪个方向行进
// 之所以 * vec3(uv, 1.0),uv.x 表示水平方向的偏移,uv.y 表示垂直方向的偏移,1.0 表示沿着相机的前进方向(即深度方向)的偏移
// vec3(uv, 1.0) 将二维的 uv 坐标扩展为一个三维向量,其中 z 分量为 1.0
vec3 cameraPos = vec3(0.0, 0.0, -2.0); // 相机位置
vec3 cameraTarget = vec3(0.0, u_mouse.y * 0.01, 0.0); // 相机看向的位置
vec3 cameraUp = vec3(0.0, 1.0, 0.0); // 相机上方向
vec3 rayDirection = getCameraMat(cameraPos, cameraTarget, cameraUp) * vec3(uv, 1.0);
vec4 result = rayMarching(cameraPos, rayDirection);
float glow = result.w; // 返回火焰的发光强度
// 方式二:
// 这个是固定视角的
// vec3 org = vec3(0.0, -2.0, 4.0);
// vec3 dir = normalize(vec3(uv.x * 1.6, -uv.y, -1.5));
// vec4 result = rayMarching(org, dir);
// float glow = result.w; // 返回火焰的发光强度
vec4 col = mix(vec4(1.0, 0.5, 0.1, 1.0), vec4(0.1, 0.5, 1.0, 1.0), result.y * 0.02 + 0.4); // 混合火焰的颜色,从橙色(底部)到蓝色(顶部)
gl_FragColor = mix(vec4(0.0), col, pow(glow * 4.0, 5.0)); // 根据发光强度调整
}`)
})
}
</script>
火 -2
点击运行
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<canvas v-if="isRunning" id="fire2" class="shader-toy-stage bg-black"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted } from 'vue'
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onStart()
} else {
isRunning.value = false
}
}
onMounted(async () => {
await nextTick()
})
const onStart = () => {
import('glslCanvas').then(module => {
const canvas = document.getElementById('fire2')
const glslCanvas: any = new module.default(canvas)
glslCanvas.load(`
#extension GL_OES_standard_derivatives: enable
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
// 将一个二维向量 p 映射到一个二维向量,这个向量的每个分量都是在 -1.0 到 1.0 之间的随机值
// hash 函数的步骤如下:
// 首先,通过 dot 函数计算 p 与两个特定向量的点积,得到两个标量值
// 然后,将这两个标量值分别乘以一个特定的常数 43758.5453123,并取其正弦值
// 最后,将正弦值乘以 2.0 并减去 1.0,得到在 -1.0 到 1.0 之间的随机值
// 这个 hash 函数在 noise 函数中被用来生成随机的梯度向量,这些梯度向量被用来计算 Perlin 噪声。在 Perlin 噪声的计算中,每个格点上的梯度向量是随机的,而 hash 函数就是用来生成这些随机梯度向量的
// 在 FBM 函数中,noise 函数被多次调用,每次调用都会生成一个噪声值,这些噪声值被加起来形成一个分形布朗运动(FBM)噪声
// FBM 噪声可以用来生成自然界的纹理,如云、山、水等
// 因此,hash 函数是生成随机梯度向量的关键函数,为 Perlin 噪声的计算提供了基础
vec2 hash(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}
// hash 函数的返回值范围:
// hash 函数返回的值在 −1.0,1.0 之间。这是因为 hash 函数通过 fract 和 sin 函数生成随机值,并将其映射到 −1.0,1.0 范围内
// 权重计算:
// 权重 h 是基于点到当前点的距离的平方计算的,并通过 max 函数确保不会为负值。权重的范围是 0.0,0.5
// 噪声值计算:
// 每个点的噪声值通过 h * h * h * h 进行平滑插值,这使得权重值更小,进一步缩小了噪声值的范围
// 最后通过 dot(n, vec3(70.0)) 进行缩放,这可能会稍微扩大返回值的范围,但仍然不会超出 −1.0,1.0
float noise(vec2 p) {
// K1 和 K2 是用于计算 Perlin 噪声的常数,它们的值是根据 Perlin 噪声的数学原理精心选择的
// 这些值的来源是 Perlin 噪声的原始论文,它们是基于正六边形的几何性质
// 在 Perlin 噪声中,每个格点上的梯度向量是随机的,而这些梯度向量的长度是 1
// 为了确保梯度向量的长度是 1,需要使用 K1 和 K2 这两个常数来调整梯度向量的坐标
// 具体来说,K1 是正六边形的边长,而 K2 是正六边形的高
// 这两个值的比值是 sqrt(3)/2,这是正六边形的几何性质决定的
const float K1 = 0.366025404; // (sqrt(3) - 1) / 2 用于将点 p 映射到 Simplex 网格中,这个值确保了网格的对称性和均匀性
const float K2 = 0.211324865; // (3 - sqrt(3)) / 6 用于调整点 a、b 和 c 的坐标,确保它们位于 Simplex 单元的正确位置
// i、a、o、b、c 的几何意义
// 在二维 Simplex 噪声中,每个点 p 被映射到一个三角形单元(Simplex)中
// 这个三角形单元由三个顶点组成:当前点 a、点 b 和点 c
// 通过计算这三个点的噪声值,并进行插值,可以生成平滑的噪声
// 计算格点坐标
// 当前点 p 所在的 Simplex 网格的格点坐标
// K1 是一个常数,用于将点 p 映射到网格中
// floor 函数用于取整,确保 i 是网格中的整数坐标
vec2 i = floor(p + (p.x + p.y) * K1);
// 计算相对坐标
// 点 p 相对于其所在的格点 i 的坐标
// K2 是另一个常数,用于调整坐标,确保 a 是在单位 Simplex 内的相对坐标
vec2 a = p - (i - (i.x + i.y) * K2);
// 确定相邻点
// 确定了当前点 p 所在的 Simplex 单元内的其他两个点 b 和 c
// o 是一个偏移向量,用于选择 b 点的方向
// 这一步的作用是根据点 a 的坐标来确定偏移向量 o
// 点 a 是当前点 p 相对于其所在格点 i 的相对坐标
// 通过比较 a.x 和 a.y 的大小,可以确定 p 在当前 Simplex 单元中的位置
// 如果 a.x<a.y,则 o=vec2(0.0,1.0)。这意味着 p 更接近于 Simplex 单元的左下角
// 如果 a.x≥a.y,则 o=vec2(1.0,0.0)。这意味着 p 更接近于 Simplex 单元的右下角
// b 和 c 是相对于 a 的其他两个顶点的坐标
vec2 o = (a.x < a.y) ? vec2(0.0, 1.0) : vec2(1.0, 0.0);
// 确定相邻点
// b 和 c 是相对于 a 的其他两个顶点的坐标
// 点 b 是 Simplex 单元内的第二个顶点
// 通过从 a 中减去偏移向量 o,然后加上常数 K2,可以得到点 b 的坐标
// 这里的 K2 是一个调整因子,用于确保 b 位于 Simplex 单元的正确位置
vec2 b = a - o + K2;
// 确定相邻点
// b 和 c 是相对于 a 的其他两个顶点的坐标
// 点 c 是 Simplex 单元内的第三个顶点
// 通过从 a 中减去 1.0,然后加上 2.0×K2,可以得到点 c 的坐标
// 这里的 1.0 和 2.0×K2 是调整因子,用于确保 c 位于 Simplex 单元的正确位置
vec2 c = a - 1.0 + 2.0 * K2;
// 计算的每个点的权重
// 权重是基于点到当前点的距离的平方,通过 dot 函数计算
// max 函数确保权重不会为负值
vec3 h = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), 0.0);
// 计算每个点的噪声值
// hash 函数用于生成随机梯度向量
// dot 函数计算点积
// h * h * h * h 是一个平滑的权重函数,用于插值
vec3 n = h * h * h * h * vec3(dot(a, hash(i + 0.0)), dot(b, hash(i + o)), dot(c, hash(i + 1.0)));
return dot(n, vec3(70.0));
}
// 遇见过多种 fbm,主要看自己想怎么写
// 1、hash 生成随机梯度向量
// 2、noise 函数的选择
// 3、fbm 计算
// ------------------------------------
// noise 函数的返回值范围:
// noise 函数返回的值在 −1.0,1.0 之间,通常集中在 −0.6,0.6 之间
// fbm 函数的结构:
// fbm 函数通过多次调用 noise 函数,并对每次的结果进行加权求和,从而生成分形布朗运动(FBM)噪声
// fbm 函数的返回值范围:
// 每次调用 noise 函数的返回值范围是 −1.0,1.0,但通常集中在 −0.6,0.6 之间
// 通过加权求和,fbm 函数的返回值范围被限制在 0.0,1.0 之间
// 最后通过 f = f + 0.5,将结果整体向上移动,使得返回值范围更接近 0.0,1.0
// ------------------------------------
// noise 函数的返回值范围是 −1.0,1.0,则:
// 第一次调用 noise(uv) 的返回值范围是 −1.0,1.0,乘以 0.5 后范围是 −0.5,0.5
// 第二次调用 noise(uv) 的返回值范围是 −1.0,1.0,乘以 0.25 后范围是 −0.25,0.25
// 第三次调用 noise(uv) 的返回值范围是 −1.0,1.0,乘以 0.125 后范围是 −0.125,0.125
// 第四次调用 noise(uv) 的返回值范围是 −1.0,1.0,乘以 0.0625 后范围是 −0.0625,0.0625
// 将这些值相加:
// 最小值:−0.5−0.25−0.125−0.0625=−1.0
// 最大值:0.5+0.25+0.125+0.0625=1.0
// f = f + 0.5 将结果整体向上移动:
// 最小值:−1.0+0.5=−0.5
// 最大值:1.0+0.5=1.5
float fbm(vec2 uv) {
float f = 0.0;
uv = uv * 2.0;
f = 0.5 * noise(uv);
uv = 2.0 * uv;
f += 0.25 * noise(uv);
uv = 2.0 * uv;
f += 0.125 * noise(uv);
uv = 2.0 * uv;
f += 0.0625 * noise(uv);
f = f + 0.5;
return f;
}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
// vec2 uv = (fragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x); // 归一化 uv 的坐标,范围在 [-1, 1] 之间
vec2 uv = fragCoord.xy /u_resolution.xy; // 归一化到 [0, 1]
uv.x *= 5.0; // 将 x 轴的范围拉伸到 [0, 5],同时会使得火苗变得更加细
uv.x -= 2.5; // 将 uv.x 的范围从 [0, 5] 偏移到 [-2.5, 2.5]。火焰的生成范围现在以 0 为中心,对称分布在屏幕的 x 轴上,即 Ox = 2.5
uv.y -= 0.25; // 与上同理,即 Oy = 0.25
// 这个的运动方向是从右上角往左下角运动
// 因为 uv 的范围是从左下角的 (-1, -1) 到右上角的 (1, 1),加上时间变量 u_time,所以会导致噪声从右上角向左下角移动
// color += fbm(uv * 4.0 + u_time * 0.5);
// 如果想改变方向,比如垂直方向,只在垂直方向上加上时间变量,这样噪声只会在垂直方向移动,而不会在水平方向移动
// color += fbm(vec2(uv.x, uv.y + u_time * 0.5) * 4.0);
// -------------------------------------------
// 为什么是“uv.y - u_time * 1.2”而不是“uv.y + u_time * 1.2”?
// 当减小 uv.y 时,纹理上的点会向下移动
// 但是,由于纹理被映射到屏幕上的一个固定区域(这个“固定区域”是重点),这些点在屏幕上的位置实际上会向上移动(因为屏幕上的 y 坐标是向上增加的)
// 这就造成了视觉上的错觉:纹理看起来像是从下往上移动
// -------------------------------------------
// 可以通过以下代码查看相关效果
// 正常情况下,每个颜色在屏幕的视觉中占领 0.2 个单位,但是红色是 -无穷大到 0.2,紫色是 0.8 到 +无穷大
// 如果 uv.y - 0.1,那么采样点在噪声纹理中向下移动了 0.1 个单位,此时红色的范围会变大,紫色的范围变小,屏幕(即上面说的“固定区域”)所展示的效果是往上滚动,坐标范围从 0~1 变成 -0.1~0.9
// void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// vec2 uv = fragCoord.xy / iResolution.xy;
// vec3 col = vec3(0.0, 0.0, 0.0);
// float y = uv.y - 0.0;
// if (y < 0.2) { col = vec3(1.0, 0.0, 0.0); }
// if (y > 0.2 && y < 0.4) { col = vec3(0.0, 1.0, 0.0); }
// if (y > 0.4 && y < 0.6) { col = vec3(0.0, 0.0, 1.0); }
// if (y > 0.6 && y < 0.8) { col = vec3(1.0, 1.0, 0.0); }
// if (y > 0.8 && y < 1.0) { col = vec3(1.0, 0.0, 1.0); }
// fragColor = vec4(col, 1.0);
// }
// -------------------------------------------
float finalFbm = fbm(vec2(uv.x, uv.y - u_time * 1.2) * 1.74588 + vec2(0.2155, 0.5654));
// 1、火焰的颜色(亮度)的变化:內焰颜色亮度最低,外焰颜色偏亮,再往外,变成了烟,亮度就又变暗了
// 2、亮度应该和像素的 y 坐标有联系
// 用 finalFbm * uv.y 来达到这一效果(有高亮部分,也有明暗的变换)
// float c = 1.0 - 1.0 * pow(finalFbm * uv.y, 1.0);
// ------------↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓------------
// 用 length(uv) 限定了只要“中间”的这一块区域,20 则是慢慢调试出来的
float c = 1.0 - 20.0 * pow(length(uv) - finalFbm * uv.y, 2.0);
// 由于 float c = 1.0 - 20.0 * pow(length(uv) - finalFbm * uv.y, 2.0); 的取值范围是 −115.568,1.0
// 所以 c1 使用 clamp 限制在 0 - 1 之间,如果输入值小于 0,则输出 0;如果输入值大于 1,则输出 1;如果输入值在 0 到 1 之间,则输出该值本身
float c1 = c * (1.0 - pow(uv.y, 4.0));
c1 = clamp(c1, 0.0, 1.0);
c1 = c1 * finalFbm; // 使得颜色正常一些,c1 乘以其他的也行
float c2 = c * (1.0 - pow(uv.y, 4.0));
vec3 color = vec3(1.5 * c1, 1.5 * c1 * c1 * c1, c1 * c1 * c1 * c1 * c1 * c1);
color = mix(vec3(0.0), color, c2); // 这个 mix 主要是防止超出 length 范围的一些噪音(火焰)出现
gl_FragColor = vec4(color, 1.0);
}`)
})
}
</script>
柏林噪音生成地图
点击运行
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<canvas v-if="isRunning" id="perlinNoiseMap" class="shader-toy-stage bg-black"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick } from 'vue'
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onStart()
} else {
isRunning.value = false
}
}
const onStart = () => {
import('glslCanvas').then(module => {
const canvas = document.getElementById('perlinNoiseMap')
const glslCanvas: any = new module.default(canvas)
const random = Math.random()
glslCanvas.load(`
#extension GL_OES_standard_derivatives: enable
precision highp float;
uniform vec2 u_resolution;
#define amp 1.9 // 噪声的振幅(amplitude),控制噪声的强度
#define fre 1.0 // 噪声的频率(frequency),控制噪声的细节程度
#define oct 5.0 // 噪声的八度数(octaves),用于生成更复杂的噪声
#define laun 2.0 // 每八度的频率倍数(lacunarity),通常为 2.0,表示每增加一个八度,频率翻倍
#define pers 0.8 // 振幅衰减率(persistence),通常为 0.8,表示每增加一个八度,振幅减少 20%,用于控制每个八度对最终噪声的贡献
#define zoom 5.0 // 用于缩放 UV 坐标,控制地形的“放大”或“缩小”程度
#define edge 1.0 // 边缘宽度,用于控制地形的边缘效果,平滑过渡
#define delta_edge 0.2 // 边缘宽度的过渡范围,用于控制边缘效果的过渡程度
#define snow vec3(0.9, 0.9, 0.9)
#define mountains vec3(0.4, 0.4, 0.2)
#define hills vec3(0.6, 0.6, 0.1)
#define plain vec3(0.1, 0.8, 0.2)
#define beach vec3(0.8, 0.8, 0.1)
#define shallow_sea vec3(0.1, 0.1, 0.9)
#define deep_sea vec3(0.1, 0.1, 0.6)
#define v_snow 0.95
#define v_mountains 0.90
#define v_hills 0.80
#define v_plain 0.70
#define v_beach 0.55
#define v_shallow_sea 0.50
#define v_deep_sea 0.30
float rand(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453 * ${random});
}
float noise(vec2 x) {
vec2 i = floor(x);
vec2 f = fract(x);
float a = rand(i);
float b = rand(i + vec2(1.0, 0.0));
float c = rand(i + vec2(0.0, 1.0));
float d = rand(i + vec2(1.0, 1.0));
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float x1 = mix(a, b, u.x);
float x2 = mix(c, d, u.x);
return mix(x1, x2, u.y);
}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
vec2 uv = (fragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);
vec2 u = fragCoord.xy / u_resolution.xy; // 将屏幕坐标归一化到 [0, 1] 范围内
float d = min(min(u.x, edge - u.x), min(u.y, edge - u.y)); // 计算当前像素到边缘的距离
float dw = smoothstep(0.0, delta_edge, d); // 使用 smoothstep 函数在边缘处平滑过渡,避免边缘突变
float val = 0.0; // 初始化噪声值 val 为 0
uv *= zoom; // 缩放 UV 坐标,控制地形的“放大”或“缩小”程度
// 使用循环生成分形噪声
// 每个八度的振幅 a 和频率 f 根据 pers 和 laun 衰减
// 将每个八度的噪声值累加到 val 中
for (float i = 0.; i < oct; i++) {
float a = amp * pow(pers, i);
float f = fre * pow(laun, i);
val += a * noise(uv * f) / oct;
}
// 将噪声值 val 乘以 dw,在边缘处平滑过渡
val *= dw;
vec3 col = vec3(0.0);
if (val < v_deep_sea) col = deep_sea;
if (val >= v_deep_sea && val < v_shallow_sea) col = shallow_sea;
if (val >= v_shallow_sea && val < v_beach) col = beach;
if (val >= v_beach && val < v_plain) col = plain;
if (val >= v_plain && val < v_hills) col = hills ;
if (val >= v_hills && val < v_mountains) col = mountains;
if (val >= v_mountains) col = snow;
gl_FragColor = vec4(col, 1.0);
}`)
})
}
</script>
水面倒影
点击运行
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<canvas v-if="isRunning" id="waterReflection" class="shader-toy-stage bg-black"></canvas>
</div>
</template>
<script lang="ts" setup>
// 整个水面倒影的实现过程,从物理场景的角度来看,主要包括以下几个关键步骤:
// 1、光线与水面的相交检测:确定光线是否与水面相交,以及交点位置。
// 2、水面法线的计算与波纹效果的添加:模拟水面的动态变化,为后续的反射计算提供基础。
// 3、反射光线的计算与行进:根据反射定律计算反射光线的方向,并检测反射光线与场景物体的交点。
// 4、菲涅尔效应的模拟:根据视角和水面法线,调整反射强度,模拟真实物理场景中的反射效果。
// 5、最终颜色的合成与输出:结合反射颜色、水面颜色、光照强度等,计算最终的水面颜色,并渲染到屏幕上。
import { ref, nextTick, onMounted } from 'vue'
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onStart()
} else {
isRunning.value = false
}
}
onMounted(async () => {
await nextTick()
})
const onStart = () => {
import('glslCanvas').then(module => {
const canvas = document.getElementById('waterReflection')
const glslCanvas: any = new module.default(canvas)
const getWaterPlaneColor = `
vec3 getWaterPlaneColor(vec3 p) {
vec3 color = vec3(1.0, 0.0, 0.0); // 红色的水面
return color;
}
`
const getWaterPlaneDist = `
// 计算一个点 p 到一个无限大水平平面(地面)的距离
// 这个平面通常被定义为 y=0 的平面,即地面位于 y 轴的零点
float getWaterPlaneDist(vec3 p) {
float planeY = p.y;
// 添加一个小的偏移量防止除零
float epsilon = 0.0001;
// 使用更稳定的水面距离计算
return p.y + epsilon; // 保持简单但稳定的计算
}
`
const getCubeDist = `
// 计算一个点 p 到一个立方体的(最近的)距离,要考虑 p 在内部和外部的情况,二者都要计算得出结果,外部返回正值,内部返回负值
// 假设立方体的半尺寸为 cubeSize = vec3(1.0, 1.0, 1.0),表示立方体在 x、y、z 轴上的半长度都为 1.0,立方体的中心位于原点 (0, 0, 0)
// 如果点 p = vec3(1.5, 1.5, 1.5),则 p 在立方体外部
// 如果点 p = vec3(0.5, 0.5, 0.5),则 p 在立方体内部
// 如果点 p = vec3(1.0, 1.0, 1.0),则 p 在立方体边界上
// 通过 getCubeDist 函数,可以计算出点 p 到立方体的最短距离
float getCubeDist(vec3 p, vec3 cubePos, vec3 cubeSize) {
// abs(p - cubePos) - cubeSize 计算点 p 相对于立方体中心 cubePos 的距离
// 首先计算点 p 与立方体中心 c 之间的差值 p − cubePos
// 然后取绝对值 abs(p − cubePos),表示点 p 相对于立方体中心的水平、垂直和深度方向的距离
// 最后减去立方体的半尺寸 cubeSize,得到 tempDist
// tempDist 的每个分量表示点在对应方向上超出立方体边界的距离
// 点在立方体内部,所有值是负值
// 点在立方体边界,至少一个是 0
// 点在立方体外,至少一个是正值
vec3 tempDist = abs(p - cubePos) - cubeSize;
// max(tempDist, 0.0) 将 tempDist 的所有负分量设置为 0,只保留正分量,表示点到立方体外部的距离(假如其中有一个是负值,则 p 是在立方体内)
// length(max(tempDist, 0.0)) 计算这个向量的长度,即点到立方体外部的最短距离
// -----------------------------------------------------------------------
// min(max(tempDist.x, max(tempDist.y, tempDist.z)), 0.0) 这部分的作用是处理点在立方体内部的情况
// max(tempDist.x, max(tempDist.y, tempDist.z)) 找出 tempDist 中最大的分量,表示点在立方体内部最深的轴向距离
// min(..., 0.0) 确保这个值不会超过 0,因为点在立方体内部时,tempDist 的所有分量都是负值
// min(max(tempDist.x, tempDist.y, tempDist.z), 0.0) 将这个最大值与 0 比较,取较小值,表示点距离立方体在某个轴上最近的面的距离为(x)个单位
// -----------------------------------------------------------------------
// 最后合并距离,将点到立方体外部的距离和点到立方体内部的距离相加,得到点到立方体的最短距离
// -----------------------------------------------------------------------
// 假如点在外部,则会使用 length(max(tempDist, 0.0))
// 假如点在内部,则会使用 min(max(tempDist.x, max(tempDist.y, tempDist.z)), 0.0)
float cubeDist = length(max(tempDist, 0.0)) + min(max(tempDist.x, max(tempDist.y, tempDist.z)), 0.0);
return cubeDist;
}
`
const getSphereDis = `
// 计算一个点 p 到一个球体的距离,球体的中心由 sphere 表示,半径由 radius 表示
float getSphereDist(vec3 p, vec3 spherePos, float radius) {
// 从当前点的位置 p 到球体中心的距离中减去球体的半径,得到点 p 到球体表面的最短距离
// 如果点 p 在球体内部,sphereDist 为负值
// 如果点 p 在球体表面,sphereDist 为零
// 如果点 p 在球体外部,sphereDist 为正值
float sphereDist = length(p - spherePos) - radius;
return sphereDist;
}
`
const getDist = `
// 取物体距离相机最近的 dist
vec2 getDist(vec3 pos) {
float waterPlaneDist = getWaterPlaneDist(pos);
vec3 SPHERE_POS_1 = vec3(0.5, 1.0, 6.0);
vec3 SPHERE_POS_2 = vec3(2.0, 1.0, 3.0);
float SPHERE_RADIUS = 1.0;
float sphereDist1 = getSphereDist(pos, SPHERE_POS_1, SPHERE_RADIUS);
float sphereDist2 = getSphereDist(pos, SPHERE_POS_2, SPHERE_RADIUS);
vec3 CUBE_POS = vec3(0.0, 0.0, 6.0);
vec3 CUBE_SIZE = vec3(1.0, 1.0, 1.0);
float cubeDist = getCubeDist(pos, CUBE_POS, CUBE_SIZE);
// 使用数组和循环找到最小距离
float distances[4];
distances[0] = waterPlaneDist;
distances[1] = sphereDist1;
distances[2] = sphereDist2;
distances[3] = cubeDist;
// 使用数组和循环找到对应的物体类型
int objectTypes[4];
objectTypes[0] = WATER_PLANE;
objectTypes[1] = SPHERE1;
objectTypes[2] = SPHERE2;
objectTypes[3] = CUBE;
// 旧版不能用这个方法
// float distances[4] = float[4](waterPlaneDist, sphereDist1, sphereDist2, cubeDist);
// int objectTypes[4] = int[4](WATER_PLANE, SPHERE1, SPHERE2, CUBE);
float minDist = distances[0];
int objectType = objectTypes[0];
for (int i = 1; i < 4; i++) {
if (distances[i] < minDist) {
minDist = distances[i];
objectType = objectTypes[i];
}
}
return vec2(minDist, float(objectType));
}
`
const getObjectColor = `
// 新增函数:根据物体类型返回基础颜色
vec3 getObjectColor(int objectType) {
// 使用 if-else 语句定义颜色映射
if (objectType == 1) {
return vec3(0.63, 0.2, 0.2);
} else if (objectType == 2) {
return vec3(1.0, 0.8, 0.2);
} else if (objectType == 3) {
return vec3(0.2, 0.7, 0.6);
} else {
return vec3(1.0); // 默认颜色
}
// 在 WebGL 1.0 (GLSL ES 1.00) 中,数组的索引必须是常量表达式,不能使用变量作为索引。这是因为 GLSL ES 1.00 的限制,它不支持动态索引。
// vec3 colors[4];
// colors[0] = vec3(0.8);
// colors[1] = vec3(1.0, 0.2, 0.2);
// colors[2] = vec3(0.2, 0.2, 1.0);
// colors[3] = vec3(0.2, 1.0, 0.2);
// return colors[objectType];
}
`
const getNormal = `
// 在基于距离场(Distance Field)的渲染中,法线(Normal)的计算是一个关键步骤
// 距离场是一种标量场,其中每个点的值表示该点到最近表面的距离
// 由于距离场本身是一个标量函数,而法线是一个向量,因此需要通过某种方式从标量场中提取出向量信息
// 有限差分法(Finite Difference Method)是一种自然且有效的方法,用于从标量场中近似计算梯度,进而得到法线
// 对于 x 方向
// grad_x ≈ [f(p + eps_x) - f(p - eps_x)] / (2 * eps) * ex
// 具体查看(自定义效果 - 两个物体的合并 -1.png)、(自定义效果 - 两个物体的合并 -2.png)、(自定义效果 - 两个物体的合并 -3.png)
vec3 getNormal(vec3 p) {
float e = 0.001;
// vec3(..., 0.0, 0.0):构造一个三维向量,只有 x 分量有效
// 通过对点 p 在 x 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 x 方向上的变化率,可以得到 x 方向上的梯度
// 结果是一个三维向量,但只有 x 分量是非零的,表示梯度在 x 方向上的分量
float x1 = getDist(p + vec3(e, 0.0, 0.0)).x;
float x2 = getDist(p - vec3(e, 0.0, 0.0)).x;
vec3 grad_x = vec3((x1 - x2) / (2.0 * e), 0.0, 0.0);
// vec3(0.0, ..., 0.0):构造一个三维向量,只有 y 分量有效
// 通过对点 p 在 y 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 y 方向上的变化率,可以得到 y 方向上的梯度
// 结果是一个三维向量,但只有 y 分量是非零的,表示梯度在 y 方向上的分量
float y1 = getDist(p + vec3(0.0, e, 0.0)).x;
float y2 = getDist(p - vec3(0.0, e, 0.0)).x;
vec3 grad_y = vec3(0.0, (y1 - y2) / (2.0 * e), 0.0);
// vec3(0.0, 0.0, ...):构造一个三维向量,只有 z 分量有效
// 通过对点 p 在 z 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 z 方向上的变化率,可以得到 z 方向上的梯度
// 结果是一个三维向量,但只有 z 分量是非零的,表示梯度在 z 方向上的分量
float z1 = getDist(p + vec3(0.0, 0.0, e)).x;
float z2 = getDist(p - vec3(0.0, 0.0, e)).x;
vec3 grad_z = vec3(0.0, 0.0, (z1 - z2) / (2.0 * e));
// 将三个方向上的梯度向量相加,得到该点的总梯度向量
// 然后使用 normalize 函数将其标准化(即长度变为 1),得到法向量
// 因为梯度向量指向函数值增长最快的方向,而对于表示表面的函数,其法向量与梯度向量方向相反(或相同,取决于表面函数的定义,但通常通过取梯度来近似法向量,并可能需要根据具体情况调整方向)
// 标准化确保了返回的法向量是一个单位向量
// return normalize(grad_x + grad_y + grad_z);
vec3 normal = normalize(grad_x + grad_y + grad_z);
return normal;
}
`
const getCameraMat = `
// 左手坐标系
// ro:相机的位置(Ray Origin),即相机在三维空间中的坐标。这个点是所有视线(或光线)的起点
// target:相机的目标点,即相机“看”向的点。这个点决定了相机的前进方向(Forward 向量)
// up:相机的向上方向(Up 向量),通常与相机的前进方向垂直,这个向量用于确定相机的右侧方向(Right 向量)和确保相机的坐标系是正交的
// 相机源点、目标、向上方向
// R、U、F 分别是 Right、Up 和 Forward 向量
mat3 getCameraMat(vec3 ro, vec3 target, vec3 up) {
vec3 f = normalize(target - ro); // 计算 Forward 向量(F)
// 叉积 cross(a, b) 的结果是一个垂直于向量 a 和 b 的向量
// 注意:由于使用的是左手坐标系,所以是使用 up 叉乘 f,而不是反过来进行叉乘,进行叉乘运算时一定要注意其方向性!
vec3 r = cross(up, f); // Right 向量(R)是 Forward 向量和 Up 向量的叉积,表示相机的右侧方向
vec3 u = normalize(cross(f, r)); // 为了确保 Up 向量垂直于 Forward 向量,需要重新计算 Up 向量为 Right 向量和 Forward 向量的叉积
return mat3(r, u, f);
}
`
const rayMarching = `
// rayOrigin 代表视线(或光线)的起点
// rayDirection 代表视线(或光线)的方向
vec2 rayMarching(vec3 rayOrigin, vec3 rayDirection) {
// disTotal 是视线(或光线)从 rayOrigin 出发后行进的总距离
float disTotal = 0.0;
const float MAX_STEPS = 100.0;
float SURFACE_DIST = 0.0001;
float objectType = -1.0;
for (float i = 0.0; i < MAX_STEPS; i += 1.0) {
// 视线(或光线)的当前位置 pos
// rayDirection * disTotal 计算视线(或光线)在方向 rayDirection 上行进距离 disTotal 后的向量,然后将这个向量加到源点 rayOrigin 上,得到新的位置 pos。
vec3 pos = rayOrigin + disTotal * rayDirection;
// dS 表示从当前视线(或光线)位置 p 到最近的场景物体表面的距离
float dS = getDist(pos).x;
disTotal += dS;
// 射中的物体类型
objectType = getDist(pos).y;
// 如果从当前位置到球面的距离 dS 小于某个阈值 SURFACE_DIST,则可能表示视线(或光线)已经“击中”了表面,因此退出循环
// 因为点 p 可能在物体内,float dS = getDist(pos).x 中的 dS 会返回负值
// 如果视线(或光线)行进的距离 disTotal 大于某个最大距离 MAX_DIST,则退出循环,可能是因为视线(或光线)已经行进得太远而没有“击中”任何物体
if (dS < SURFACE_DIST || disTotal > MAX_DIST) {
break;
}
}
// 返回是否命中的标志,未命中的,则是天空色
bool hit = disTotal < MAX_DIST;
return vec2(disTotal, hit ? objectType : -1.0);
}
`
const getLightDif = `
// 计算一个 3D 点 p 与光源之间的漫反射光照强度
float getLightDif(vec3 lightPos, vec3 p) {
float SHADOW = 0.1;
// 计算从点 p 到光源的方向,方向的箭头指向 lightPos
// 首先计算从点 p 到光源 lightPos 的向量
// 然后,使用 normalize 函数将这个向量标准化(或归一化),使其长度为 1
// 标准化后的向量 l 表示,从点 p 指向光源的方向
vec3 l = normalize(lightPos - p);
vec3 n = getNormal(p);
// 计算漫反射光照强度
// 使用点积(dot 函数)来计算法线向量 n 和光源方向向量 l 之间的角度的余弦值
// 这个余弦值表示了光源方向和表面法线之间的“对齐”程度,从而决定了光照的强度
// 由于余弦值可能是负的(当光源在表面的背面时),使用 max 函数确保结果始终是非负的
// 因此,dif 变量存储了漫反射光照的强度
float dif = max(dot(n, l), 0.0);
// 如果不需要阴影
// return dif;
// 阴影检测(偏移起点防止自相交)
float dis = rayMarching(p + 0.01 * n, l).x;
if (dis < length(lightPos - p)) {
// 存在遮挡则减弱光照
dif *= SHADOW;
}
return dif;
}
`
const getSkyColor = `
// 根据给定的方向向量 eye 计算天空的颜色这个函数模拟了天空的渐变效果,从地平线到天顶的颜色变化
vec3 getSkyColor(vec3 eye) {
// 方案1 -------------------------------------------------------------------
// max(e.y, 0.0):确保 e.y 的值不小于 0,因为天空的颜色通常在地平线以上
// max(e.y, 0.0) * 0.8:将 e.y 的值乘以 0.8,使颜色变化更加平缓
// max(e.y, 0.0) * 0.8 + 0.2:在乘以 0.8 的基础上加上 0.2,确保地平线处的颜色不会太暗
// (max(e.y, 0.0) * 0.8 + 0.2) * 0.8:再次乘以 0.8,进一步调整颜色的亮度
eye.y = (max(eye.y, 0.0) * 0.8 + 0.2) * 0.8;
// pow(1.0 - eye.y, 2.0):计算 1.0 - eye.y 的平方,用于模拟天空颜色的非线性变化这个值将用于红色分量,使天空在地平线处更红
// 1.0 - eye.y:直接使用 1.0 - eye.y 作为绿色分量,使天空在地平线处更绿
// 0.6 + (1.0 - eye.y) * 0.4:计算蓝色分量,使天空在地平线处更蓝 0.6 是基础蓝色,(1.0 - eye.y) * 0.4 根据 eye.y 的值增加蓝色
// vec3(pow(1.0 - eye.y, 2.0), 1.0 - eye.y, 0.6 + (1.0 - eye.y) * 0.4):将计算得到的红、绿、蓝分量组合成一个颜色向量
// vec3(pow(1.0 - eye.y, 2.0), 1.0 - eye.y, 0.6 + (1.0 - eye.y) * 0.4) * 1.1:将颜色向量乘以 1.1,增加颜色的亮度
return vec3(pow(1.0 - eye.y, 2.0), 1.0 - eye.y, 0.6 + (1.0 - eye.y) * 0.4) * 1.1;
// 方案2 -------------------------------------------------------------------
// // 归一化 eye.y 到 [0, 1] 范围
// eye.y = (eye.y + 1.0) * 0.5;
// // 使用更平滑的渐变公式
// float t = clamp(eye.y * 0.5 + 0.5, 0.0, 1.0);
// vec3 skyTop = vec3(0.2, 0.5, 1.0); // 天顶颜色(亮蓝色)
// vec3 skyHorizon = vec3(0.8, 0.8, 0.9); // 地平线颜色(亮白色)
// // 使用平滑过渡
// return mix(skyHorizon, skyTop, pow(t, 0.5));
}
`
glslCanvas.load(`
#extension GL_OES_standard_derivatives: enable
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
const float MAX_DIST = 1000.0;
// 定义物体类型枚举
#define WATER_PLANE 0
#define SPHERE1 1
#define SPHERE2 2
#define CUBE 3
${getWaterPlaneColor}
${getWaterPlaneDist}
${getCubeDist}
${getSphereDis}
${getDist}
${getObjectColor}
${getNormal}
${getCameraMat}
${rayMarching}
${getLightDif}
${getSkyColor}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
// 归一化 uv 的坐标范围到 [-1, 1]
// [0, 1] -> [-0.5, 0.5]<* 0.5>
vec2 uv = (fragCoord.xy - u_resolution.xy * 0.5) / min(u_resolution.y, u_resolution.x);
// 这行会导致格子样式错乱
// uv *= 5.0;
vec3 finalColor = vec3(0.0);
// 相机信息
vec3 cameraPos = vec3(0.0, 2.0, -8.0);
vec3 cameraUp = vec3(0.0, 1.0, 0.0);
vec3 cameraTarget = vec3(0.0, 0.0, 0.0);
// 光源的位置
vec3 lightPos = vec3(3.0 + sin(u_time), 8.0, -5.0 + cos(u_time));
vec3 lightColor = vec3(1.0, 1.0, 1.0);
// rayDirection 是视线(或光线)的方向向量,表示视线(或光线)沿着哪个方向行进
// 之所以 * vec3(uv, 1.0),uv.x 表示水平方向的偏移,uv.y 表示垂直方向的偏移,1.0 表示沿着相机的前进方向(即深度方向)的偏移
// 1.0 表示沿着相机的前进方向(即深度方向)的偏移
// vec3(uv, 1.0) 将二维的 uv 坐标扩展为一个三维向量,其中 z 分量为 1.0
vec3 rayDirection = getCameraMat(cameraPos, cameraTarget, cameraUp) * vec3(uv, 1.0);
// 射中物体的结果
vec2 rayResult = rayMarching(cameraPos, rayDirection);
// 视线(或光线)步进行进了多少距离
float rayDist = rayResult.x;
// 射中的当前的物体
float objectType = rayResult.y;
// 视线(或光线)的当前位置 p(从相机发出一条射线,一直延伸到接触了物体表面,这之间的距离)
// rayDirection * rayDist 计算视线(或光线在方向 rayDirection 上行进距离 rayDist 后的向量,然后将这个向量加到源点 cameraTarget 上,得到新的位置 p。
vec3 pointOfCameraTouchObject = cameraPos + rayDirection * rayDist;
// 点 p 与光源之间的漫反射光照强度
float lightDif = getLightDif(lightPos, pointOfCameraTouchObject);
// 获取物体颜色
vec3 objectColor = vec3(0.0);
if(int(objectType) == -1) { // 如果没有碰到物体或者水面,使用天空颜色
finalColor = getSkyColor(normalize(rayDirection)); // 直接使用天空色
} else if(int(objectType) == 0) { // 碰撞到水面,对水面进行反射计算
// 1、水面的法线
// 水面法线的计算基于距离场的梯度,使用有限差分法近似计算
// ∇f(p) ≈ (f(p + ϵ) - f(p - ϵ)) / 2ϵ f(p) 是距离场函数,表示点 p 到最近表面的距离。ϵ 是一个小的偏移量,用于数值计算
// 2、反射光线方向
// 反射定律描述了光线在光滑表面上的反射行为,即入射角等于反射角
// 公式是 reflectDir = rayDirection - 2 * (rayDirection ⋅ waterNormal) * waterNormal
// 3、反射光线的追踪
// 4、菲涅尔效应
// 菲涅尔效应描述了光线在不同入射角下的反射强度变化
// 公式是 R(θ)=R0 + (1 - R0) * (1 - cosθ)^5
// R(θ) 是反射率
// R0是法线方向上的反射率,通常为((n1 - n2) / (n1 + n2))^2,其中 n1和 n2分别是两种介质的折射率
// θ 是入射角
// 改案例简化了菲涅尔公式,使用了 (1 - cosθ)^2来近似反射率的变化,并通过 clamp 函数限制了反射强度的范围
// 5、光照模型
// 动态水面法线(这里可以添加波纹噪声计算)
vec3 waterNormal = normalize(getNormal(pointOfCameraTouchObject));
// 调整水面法线以创建波纹效果
float waveHeight = 0.05; // 波纹高度
float waveFreq = 1.0; // 波纹频率
waterNormal.y += sin(pointOfCameraTouchObject.x * waveFreq + u_time * 0.5) * waveHeight * 0.5;
waterNormal.y += sin(pointOfCameraTouchObject.z * waveFreq * 0.5 + u_time * 0.5) * waveHeight * 0.5;
waterNormal = normalize(waterNormal);
// 增加反射距离限制
float reflectMaxDist = 100.0;
// 反射光线方向
vec3 reflectDir = reflect(rayDirection, waterNormal);
// 反射光线的追踪
vec2 reflectResult = rayMarching(pointOfCameraTouchObject + waterNormal * 0.01, reflectDir);
// 反射点的颜色
vec3 reflectColor = vec3(0.0);
if(reflectResult.y > -1.0 && reflectResult.x < reflectMaxDist) {
// 反射物体颜色
vec3 reflectObjColor = getObjectColor(int(reflectResult.y));
// 反射光照计算
float reflectLightDif = getLightDif(lightPos, pointOfCameraTouchObject + reflectDir * reflectResult.x);
reflectColor = reflectObjColor * lightColor * reflectLightDif;
} else {
// 天空反射
reflectColor = getSkyColor(normalize(reflectDir));
}
// 水面基础颜色
vec3 waterColor = getWaterPlaneColor(pointOfCameraTouchObject);
// 菲涅尔效应(根据视角调整反射强度)
float fresnel = pow(1.0 - abs(dot(rayDirection, waterNormal)), 2.0);
fresnel = clamp(fresnel, 0.3, 0.9); // 限制反射强度范围
// 混合反射颜色和水面颜色
vec3 finalReflectColor = mix(waterColor, reflectColor, fresnel);
// 添加环境光
float ambient = 0.2;
objectColor = finalReflectColor * (lightDif + ambient);
finalColor = objectColor * lightColor * max(lightDif, 0.1); // 确保最小光照强度;
} else { // 碰撞到物体
objectColor = getObjectColor(int(objectType));
// 计算最终颜色(物体颜色 * 光照颜色 * 光照强度)
finalColor = objectColor * lightColor * lightDif;
}
gl_FragColor = vec4(finalColor, 1.0);
}
`)
})
}
</script>
物体赋予各自颜色
点击运行
使用struct优化
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<div>使用struct优化</div>
<canvas v-if="isRunning" id="moreGeoWithColor" class="shader-toy-stage bg-black"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted } from 'vue'
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onStart()
} else {
isRunning.value = false
}
}
onMounted(async () => {
await nextTick()
// isRunning.value = true
// await nextTick()
// onStart()
})
const onStart = () => {
import('glslCanvas').then(module => {
const canvas = document.getElementById('moreGeoWithColor')
const glslCanvas: any = new module.default(canvas)
const groundGrid = `
// 黑白网格
vec3 groundGrid(vec3 p) {
vec3 black = vec3(1.0);
vec3 white = vec3(0.0);
// 远处的格子会变形
// floor(p.x) 和 floor(p.z) 分别取 p.x 和 p.z 的整数部分
// mod(..., 2.0) 取上述和对 2 的模,结果为 0 或 1
float m = mod(floor(p.x) + floor(p.z), 2.0);
// mix(black, white, m) 根据 m 的值在 black 和 white 之间进行插值
// 当 m 为 0 时,返回 white(白色)
// 当 m 为 1 时,返回 black(黑色)
vec3 groundColor = mix(black, white, m);
return groundColor;
}
`
const getGroundDist = `
// 计算一个点 p 到一个无限大水平平面(地面)的距离
// 这个平面通常被定义为 y=0 的平面,即地面位于 y 轴的零点
float getGroundDist(vec3 p) {
float groundY = p.y;
return groundY;
}
`
const getCubeDist = `
// 计算一个点 p 到一个立方体的(最近的)距离,要考虑 p 在内部和外部的情况,二者都要计算得出结果,外部返回正值,内部返回负值
// 假设立方体的半尺寸为 cubeSize = vec3(1.0, 1.0, 1.0),表示立方体在 x、y、z 轴上的半长度都为 1.0,立方体的中心位于原点 (0, 0, 0)
// 如果点 p = vec3(1.5, 1.5, 1.5),则 p 在立方体外部
// 如果点 p = vec3(0.5, 0.5, 0.5),则 p 在立方体内部
// 如果点 p = vec3(1.0, 1.0, 1.0),则 p 在立方体边界上
// 通过 getCubeDist 函数,可以计算出点 p 到立方体的最短距离
float getCubeDist(vec3 p, vec3 cubePos, vec3 cubeSize) {
// abs(p - cubePos) - cubeSize 计算点 p 相对于立方体中心 cubePos 的距离
// 首先计算点 p 与立方体中心 c 之间的差值 p − cubePos
// 然后取绝对值 abs(p − cubePos),表示点 p 相对于立方体中心的水平、垂直和深度方向的距离
// 最后减去立方体的半尺寸 cubeSize,得到 tempDist
// tempDist 的每个分量表示点在对应方向上超出立方体边界的距离
// 点在立方体内部,所有值是负值
// 点在立方体边界,至少一个是 0
// 点在立方体外,至少一个是正值
vec3 tempDist = abs(p - cubePos) - cubeSize;
// max(tempDist, 0.0) 将 tempDist 的所有负分量设置为 0,只保留正分量,表示点到立方体外部的距离(假如其中有一个是负值,则 p 是在立方体内)
// length(max(tempDist, 0.0)) 计算这个向量的长度,即点到立方体外部的最短距离
// -----------------------------------------------------------------------
// min(max(tempDist.x, max(tempDist.y, tempDist.z)), 0.0) 这部分的作用是处理点在立方体内部的情况
// max(tempDist.x, max(tempDist.y, tempDist.z)) 找出 tempDist 中最大的分量,表示点在立方体内部最深的轴向距离
// min(..., 0.0) 确保这个值不会超过 0,因为点在立方体内部时,tempDist 的所有分量都是负值
// min(max(tempDist.x, tempDist.y, tempDist.z), 0.0) 将这个最大值与 0 比较,取较小值,表示点距离立方体在某个轴上最近的面的距离为(x)个单位
// -----------------------------------------------------------------------
// 最后合并距离,将点到立方体外部的距离和点到立方体内部的距离相加,得到点到立方体的最短距离
// -----------------------------------------------------------------------
// 假如点在外部,则会使用 length(max(tempDist, 0.0))
// 假如点在内部,则会使用 min(max(tempDist.x, max(tempDist.y, tempDist.z)), 0.0)
float cubeDist = length(max(tempDist, 0.0)) + min(max(tempDist.x, max(tempDist.y, tempDist.z)), 0.0);
return cubeDist;
}
`
const getSphereDis = `
// 计算一个点 p 到一个球体的距离,球体的中心由 sphere 表示,半径由 radius 表示
float getSphereDist(vec3 p, vec3 spherePos, float radius) {
// 从当前点的位置 p 到球体中心的距离中减去球体的半径,得到点 p 到球体表面的最短距离
// 如果点 p 在球体内部,sphereDist 为负值
// 如果点 p 在球体表面,sphereDist 为零
// 如果点 p 在球体外部,sphereDist 为正值
float sphereDist = length(p - spherePos) - radius;
return sphereDist;
}
`
const getDist = `
// 取物体距离相机最近的 dist
vec2 getDist(vec3 pos) {
float groundDist = getGroundDist(pos);
vec3 SPHERE_POS_1 = vec3(0.5, 1.0, 6.0);
vec3 SPHERE_POS_2 = vec3(2.0, 1.0, 3.0);
float SPHERE_RADIUS = 1.0;
float sphereDist1 = getSphereDist(pos, SPHERE_POS_1, SPHERE_RADIUS);
float sphereDist2 = getSphereDist(pos, SPHERE_POS_2, SPHERE_RADIUS);
vec3 CUBE_POS = vec3(0.0, 0.0, 6.0);
vec3 CUBE_SIZE = vec3(1.0, 1.0, 1.0);
float cubeDist = getCubeDist(pos, CUBE_POS, CUBE_SIZE);
// 使用数组和循环找到最小距离
float distances[4];
distances[0] = groundDist;
distances[1] = sphereDist1;
distances[2] = sphereDist2;
distances[3] = cubeDist;
// 使用数组和循环找到对应的物体类型
int objectTypes[4];
objectTypes[0] = GROUND;
objectTypes[1] = SPHERE1;
objectTypes[2] = SPHERE2;
objectTypes[3] = CUBE;
// float distances[4] = float[4](groundDist, sphereDist1, sphereDist2, cubeDist);
// int objectTypes[4] = int[4](GROUND, SPHERE1, SPHERE2, CUBE);
float minDist = distances[0];
int objectType = objectTypes[0];
for (int i = 1; i < 4; i++) {
if (distances[i] < minDist) {
minDist = distances[i];
objectType = objectTypes[i];
}
}
return vec2(minDist, float(objectType));
}
`
const getObjectColor = `
// 新增函数:根据物体类型返回基础颜色
vec3 getObjectColor(int objectType) {
// 使用 if-else 语句定义颜色映射
if (objectType == 0) {
return vec3(0.8);
} else if (objectType == 1) {
return vec3(0.63, 0.2, 0.2);
} else if (objectType == 2) {
return vec3(1.0, 0.8, 0.2);
} else if (objectType == 3) {
return vec3(0.2, 0.7, 0.6);
} else {
return vec3(1.0); // 默认颜色
}
// 在 WebGL 1.0 (GLSL ES 1.00) 中,数组的索引必须是常量表达式,不能使用变量作为索引。这是因为 GLSL ES 1.00 的限制,它不支持动态索引。
// vec3 colors[4];
// colors[0] = vec3(0.8);
// colors[1] = vec3(1.0, 0.2, 0.2);
// colors[2] = vec3(0.2, 0.2, 1.0);
// colors[3] = vec3(0.2, 1.0, 0.2);
// return colors[objectType];
}
`
const getNormal = `
// 在基于距离场(Distance Field)的渲染中,法线(Normal)的计算是一个关键步骤
// 距离场是一种标量场,其中每个点的值表示该点到最近表面的距离
// 由于距离场本身是一个标量函数,而法线是一个向量,因此需要通过某种方式从标量场中提取出向量信息
// 有限差分法(Finite Difference Method)是一种自然且有效的方法,用于从标量场中近似计算梯度,进而得到法线
// 对于 x 方向
// grad_x ≈ [f(p + eps_x) - f(p - eps_x)] / (2 * eps) * ex
// 具体查看(自定义效果 - 两个物体的合并 -1.png)、(自定义效果 - 两个物体的合并 -2.png)、(自定义效果 - 两个物体的合并 -3.png)
vec3 getNormal(vec3 p) {
float e = 0.001;
// vec3(..., 0.0, 0.0):构造一个三维向量,只有 x 分量有效
// 通过对点 p 在 x 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 x 方向上的变化率,可以得到 x 方向上的梯度
// 结果是一个三维向量,但只有 x 分量是非零的,表示梯度在 x 方向上的分量
float x1 = getDist(p + vec3(e, 0.0, 0.0)).x;
float x2 = getDist(p - vec3(e, 0.0, 0.0)).x;
vec3 grad_x = vec3((x1 - x2) / (2.0 * e), 0.0, 0.0);
// vec3(0.0, ..., 0.0):构造一个三维向量,只有 y 分量有效
// 通过对点 p 在 y 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 y 方向上的变化率,可以得到 y 方向上的梯度
// 结果是一个三维向量,但只有 y 分量是非零的,表示梯度在 y 方向上的分量
float y1 = getDist(p + vec3(0.0, e, 0.0)).x;
float y2 = getDist(p - vec3(0.0, e, 0.0)).x;
vec3 grad_y = vec3(0.0, (y1 - y2) / (2.0 * e), 0.0);
// vec3(0.0, 0.0, ...):构造一个三维向量,只有 z 分量有效
// 通过对点 p 在 z 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 z 方向上的变化率,可以得到 z 方向上的梯度
// 结果是一个三维向量,但只有 z 分量是非零的,表示梯度在 z 方向上的分量
float z1 = getDist(p + vec3(0.0, 0.0, e)).x;
float z2 = getDist(p - vec3(0.0, 0.0, e)).x;
vec3 grad_z = vec3(0.0, 0.0, (z1 - z2) / (2.0 * e));
// 将三个方向上的梯度向量相加,得到该点的总梯度向量
// 然后使用 normalize 函数将其标准化(即长度变为 1),得到法向量
// 因为梯度向量指向函数值增长最快的方向,而对于表示表面的函数,其法向量与梯度向量方向相反(或相同,取决于表面函数的定义,但通常通过取梯度来近似法向量,并可能需要根据具体情况调整方向)
// 标准化确保了返回的法向量是一个单位向量
return normalize(grad_x + grad_y + grad_z);
}
`
const getCameraMat = `
// 左手坐标系
// ro:相机的位置(Ray Origin),即相机在三维空间中的坐标。这个点是所有视线(或光线)的起点
// target:相机的目标点,即相机“看”向的点。这个点决定了相机的前进方向(Forward 向量)
// up:相机的向上方向(Up 向量),通常与相机的前进方向垂直,这个向量用于确定相机的右侧方向(Right 向量)和确保相机的坐标系是正交的
// 相机源点、目标、向上方向
// R、U、F 分别是 Right、Up 和 Forward 向量
mat3 getCameraMat(vec3 ro, vec3 target, vec3 up) {
vec3 f = normalize(target - ro); // 计算 Forward 向量(F)
// 叉积 cross(a, b) 的结果是一个垂直于向量 a 和 b 的向量
// 注意:由于使用的是左手坐标系,所以是使用 up 叉乘 f,而不是反过来进行叉乘,进行叉乘运算时一定要注意其方向性!
vec3 r = cross(up, f); // Right 向量(R)是 Forward 向量和 Up 向量的叉积,表示相机的右侧方向
vec3 u = normalize(cross(f, r)); // 为了确保 Up 向量垂直于 Forward 向量,需要重新计算 Up 向量为 Right 向量和 Forward 向量的叉积
return mat3(r, u, f);
}
`
const rayMarching = `
// rayOrigin 代表视线(或光线)的起点
// rayDirection 代表视线(或光线)的方向
vec2 rayMarching(vec3 rayOrigin, vec3 rayDirection) {
// disTotal 是视线(或光线)从 rayOrigin 出发后行进的总距离
float disTotal = 0.0;
const float MAX_STEPS = 100.0;
float SURFACE_DIST = 0.0001;
float objectType = -1.0;
for (float i = 0.0; i < MAX_STEPS; i += 1.0) {
// 视线(或光线)的当前位置 pos
// rayDirection * disTotal 计算视线(或光线)在方向 rayDirection 上行进距离 disTotal 后的向量,然后将这个向量加到源点 rayOrigin 上,得到新的位置 pos。
vec3 pos = rayOrigin + disTotal * rayDirection;
// dS 表示从当前视线(或光线)位置 p 到最近的场景物体表面的距离
float dS = getDist(pos).x;
disTotal += dS;
// 射中的物体类型
objectType = getDist(pos).y;
// 如果从当前位置到球面的距离 dS 小于某个阈值 SURFACE_DIST,则可能表示视线(或光线)已经“击中”了表面,因此退出循环
// 因为点 p 可能在物体内,float dS = getDist(pos).x 中的 dS 会返回负值
// 如果视线(或光线)行进的距离 disTotal 大于某个最大距离 MAX_DIST,则退出循环,可能是因为视线(或光线)已经行进得太远而没有“击中”任何物体
if (dS < SURFACE_DIST || disTotal > MAX_DIST) {
break;
}
}
// 返回是否命中的标志,未命中的,则是天空色
bool hit = disTotal < MAX_DIST;
return vec2(disTotal, hit ? objectType : -1.0);
}
`
const getLightDif = `
// 计算一个 3D 点 p 与光源之间的漫反射光照强度
float getLightDif(vec3 lightPos, vec3 p) {
float SHADOW = 0.1;
// 计算从点 p 到光源的方向,方向的箭头指向 lightPos
// 首先计算从点 p 到光源 lightPos 的向量
// 然后,使用 normalize 函数将这个向量标准化(或归一化),使其长度为 1
// 标准化后的向量 l 表示,从点 p 指向光源的方向
vec3 l = normalize(lightPos - p);
vec3 n = getNormal(p);
// 计算漫反射光照强度
// 使用点积(dot 函数)来计算法线向量 n 和光源方向向量 l 之间的角度的余弦值
// 这个余弦值表示了光源方向和表面法线之间的“对齐”程度,从而决定了光照的强度
// 由于余弦值可能是负的(当光源在表面的背面时),使用 max 函数确保结果始终是非负的
// 因此,dif 变量存储了漫反射光照的强度
float dif = max(dot(n, l), 0.0);
// 如果不需要阴影
// return dif;
// 阴影检测(偏移起点防止自相交)
float dis = rayMarching(p + 0.01 * n, l).x;
if (dis < length(lightPos - p)) {
// 存在遮挡则减弱光照
dif *= SHADOW;
}
return dif;
}
`
const getSkyColor = `
// 根据给定的方向向量 eye 计算天空的颜色这个函数模拟了天空的渐变效果,从地平线到天顶的颜色变化
vec3 getSkyColor(vec3 eye) {
// 方案1 -------------------------------------------------------------------
// max(e.y, 0.0):确保 e.y 的值不小于 0,因为天空的颜色通常在地平线以上
// max(e.y, 0.0) * 0.8:将 e.y 的值乘以 0.8,使颜色变化更加平缓
// max(e.y, 0.0) * 0.8 + 0.2:在乘以 0.8 的基础上加上 0.2,确保地平线处的颜色不会太暗
// (max(e.y, 0.0) * 0.8 + 0.2) * 0.8:再次乘以 0.8,进一步调整颜色的亮度
eye.y = (max(eye.y, 0.0) * 0.8 + 0.2) * 0.8;
// pow(1.0 - eye.y, 2.0):计算 1.0 - eye.y 的平方,用于模拟天空颜色的非线性变化这个值将用于红色分量,使天空在地平线处更红
// 1.0 - eye.y:直接使用 1.0 - eye.y 作为绿色分量,使天空在地平线处更绿
// 0.6 + (1.0 - eye.y) * 0.4:计算蓝色分量,使天空在地平线处更蓝 0.6 是基础蓝色,(1.0 - eye.y) * 0.4 根据 eye.y 的值增加蓝色
// vec3(pow(1.0 - eye.y, 2.0), 1.0 - eye.y, 0.6 + (1.0 - eye.y) * 0.4):将计算得到的红、绿、蓝分量组合成一个颜色向量
// vec3(pow(1.0 - eye.y, 2.0), 1.0 - eye.y, 0.6 + (1.0 - eye.y) * 0.4) * 1.1:将颜色向量乘以 1.1,增加颜色的亮度
return vec3(pow(1.0 - eye.y, 2.0), 1.0 - eye.y, 0.6 + (1.0 - eye.y) * 0.4) * 1.1;
// 方案2 -------------------------------------------------------------------
// // 更自然的天空渐变
// float t = clamp(eye.y * 0.5 + 0.5, 0.0, 1.0);
// vec3 skyTop = vec3(0.2, 0.5, 1.0) * 1.2; // 天顶颜色(亮蓝色)
// vec3 skyHorizon = vec3(0.8, 0.8, 0.9) * 1.0; // 地平线颜色(亮白色)
// // 使用平滑过渡
// return mix(skyHorizon, skyTop, pow(t, 0.5));
}
`
glslCanvas.load(`
#extension GL_OES_standard_derivatives: enable
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
const float MAX_DIST = 1000.0;
// 定义物体类型枚举
#define GROUND 0
#define SPHERE1 1
#define SPHERE2 2
#define CUBE 3
${groundGrid}
${getGroundDist}
${getCubeDist}
${getSphereDis}
${getDist}
${getObjectColor}
${getNormal}
${getCameraMat}
${rayMarching}
${getLightDif}
${getSkyColor}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
// 归一化 uv 的坐标范围到 [-1, 1]
// [0, 1] -> [-0.5, 0.5]<* 0.5>
vec2 uv = (fragCoord.xy - u_resolution.xy * 0.5) / min(u_resolution.y, u_resolution.x);
// 这行会导致格子样式错乱
// uv *= 5.0;
vec3 finalColor = vec3(0.0);
// 相机信息
vec3 cameraPos = vec3(0.0, 1.0, -8.0);
vec3 cameraUp = vec3(0.0, 1.0, 0.0);
vec3 cameraTarget = vec3(0.0, 1.0, 0.0);
// 光源的位置
vec3 lightPos = vec3(3.0 * cos(u_time), 8.0, -5.0 * sin(u_time));
vec3 lightColor = vec3(1.0, 1.0, 1.0);
// rayDirection 是视线(或光线)的方向向量,表示视线(或光线)沿着哪个方向行进
// 之所以 * vec3(uv, 1.0),uv.x 表示水平方向的偏移,uv.y 表示垂直方向的偏移,1.0 表示沿着相机的前进方向(即深度方向)的偏移
// 1.0 表示沿着相机的前进方向(即深度方向)的偏移
// vec3(uv, 1.0) 将二维的 uv 坐标扩展为一个三维向量,其中 z 分量为 1.0
vec3 rayDirection = getCameraMat(cameraPos, cameraTarget, cameraUp) * vec3(uv, 1.0);
// 天空颜色
vec3 skyColor = getSkyColor(normalize(rayDirection));
// 射中物体的结果
vec2 rayResult = rayMarching(cameraPos, rayDirection);
// 视线(或光线)步进行进了多少距离
float rayDist = rayResult.x;
// 射中的当前的物体
float objectType = rayResult.y;
// 获取物体颜色
vec3 objectColor = getObjectColor(int(objectType));
if(int(objectType) == -1) { // 未命中任何物体
// float fog = smoothstep(0.0, 50.0, rayDist); // 根据距离添加大气效果
// finalColor = mix(skyColor, vec3(0.7, 0.8, 0.9), fog); // 根据距离添加大气效果
finalColor = skyColor; // 直接使用天空色
} else { // 命中物体,计算光照和颜色
// 视线(或光线)的当前位置 p(从相机发出一条射线,一直延伸到接触了物体表面,这之间的距离)
// rayDirection * rayDist 计算视线(或光线在方向 rayDirection 上行进距离 rayDist 后的向量,然后将这个向量加到源点 cameraTarget 上,得到新的位置 p。
vec3 pointOfCameraTouchObject = cameraPos + rayDirection * rayDist;
// 点 p 与光源之间的漫反射光照强度
float lightDif = getLightDif(lightPos, pointOfCameraTouchObject);
// 计算最终颜色(物体颜色 * 光照颜色 * 光照强度)
vec3 litColor = objectColor * lightColor * lightDif;
vec3 materialColor = vec3(0.0);
if (int(objectType) == GROUND) {
materialColor = groundGrid(pointOfCameraTouchObject) * litColor;
} else {
materialColor = litColor; // 对于非地面物体,直接使用 litColor
}
// 当 rayDirection.y > 10.0 则会返回0的,pow(0, 0.2)等于0,则mix结果是 skyColor
// 当 rayDirection.y < 0 则会返回1,结果是返回 materialColor
// 其余则是混合两者的颜色
finalColor = mix(
skyColor,
materialColor,
pow(smoothstep(10.0, 0.0, rayDirection.y), 1.0)
);
}
gl_FragColor = vec4(finalColor, 1.0);
}
`)
})
}
</script>
下雨的雨水波纹
点击运行
注意:这里雨点滴下来中的“减法”,和fire2中的 “float finalFbm = fbm(vec2(uv.x, uv.y - u_time * 1.2) * 1.74588 + vec2(0.2155, 0.5654));” 的“减法”的区别。
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<div class="color-red">注意:这里雨点滴下来中的“减法”,和fire2中的 “float finalFbm = fbm(vec2(uv.x, uv.y - u_time * 1.2) * 1.74588 + vec2(0.2155, 0.5654));” 的“减法”的区别。</div>
<canvas v-if="isRunning" id="rain" class="shader-toy-stage bg-black"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted } from 'vue'
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onStart()
} else {
isRunning.value = false
}
}
onMounted(async () => {
await nextTick()
})
const onStart = () => {
import('glslCanvas').then(module => {
const canvas = document.getElementById('rain')
const glslCanvas: any = new module.default(canvas)
glslCanvas.load(`
#extension GL_OES_standard_derivatives: enable
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
#define WATER_PLANE 0;
#define PI 3.1415926
const float MAX_DIST = 1000.0;
const float rainNum = 20.0;
float random1d(float dt) {
float c = 43758.5453;
float sn = mod(dt, 3.14);
return fract(sin(sn) * c);
}
// pos与屏幕的分辨率相关
vec2 randomRainPos(float val, vec2 u_resolution, vec2 velocity) {
// 计算雨滴在水平方向上可能的最大偏移量 maxX
// velocity.x 是雨滴水平方向上的速度分量
// abs(u_resolution.y / velocity.y) 计算的是雨滴从屏幕顶部下落到屏幕底部的过程中,水平方向上可能移动的最大距离的比例因子
// maxX 表示雨滴在水平方向上可能的最大偏移量,这个偏移量取决于雨滴的速度和屏幕的高度
float maxX = velocity.x * abs(u_resolution.y / velocity.y);
// 计算雨滴的水平位置 x
// step(0.0, maxX) 是一个阶跃函数,当 maxX 大于 0.0 时返回 1.0,否则返回 0.0。这里的作用是确保 maxX 是一个正数
// -maxX * step(0.0, maxX) 用于确定雨滴水平位置的最小值,即 -maxX
// u_resolution.x + abs(maxX) 是雨滴水平位置的最大值,即屏幕宽度加上雨滴可能的最大水平偏移量
// random1d(val) 是一个随机数生成函数,根据输入的种子值 val 生成一个 [0, 1) 范围内的随机数
// x 的值是通过将随机数缩放到 [-maxX, u_resolution.x + abs(maxX)] 的范围内得到的,这样可以确保雨滴的水平位置是随机的,并且在屏幕范围内
float x = -maxX * step(0.0, maxX) + (u_resolution.x + abs(maxX)) * random1d(val);
// 计算雨滴的垂直位置 y
// random1d(1.234 * val) 是一个随机数生成函数,根据输入的种子值 1.234 * val 生成一个 [0, 1) 范围内的随机数
// 0.05 * random1d(1.234 * val) 将随机数缩放到 [0, 0.05) 的范围内
// 1.0 + 0.05 * random1d(1.234 * val) 将随机数偏移 1.0,得到一个 [1.0, 1.05) 范围内的随机数
// y 的值是通过将这个随机数乘以屏幕高度 u_resolution.y 得到的,这样可以确保雨滴的垂直位置在屏幕高度的 [u_resolution.y, 1.05 * u_resolution.y] 范围内
// 这意味着雨滴的起始位置会略高于屏幕顶部,这样可以避免雨滴从屏幕边缘出现时看起来不自然
float y = (1.0 + 0.05 * random1d(1.234 * val)) * u_resolution.y;
return vec2(x, y);
}
// rainSize
// rainLen
// currentPos:当前的屏幕像素,经过 elapsedTime 秒后,所在的位置
// fragCoord:当前的屏幕像素
// velocityDir:雨滴运动方向的单位向量
vec3 drawRain(float rainSize, float rainLen, vec2 currentPos, vec2 fragCoord, vec2 velocityDir) {
// 这两行的计算原理如下:
// 1、gpu的并行计算的,所以是 fragCoord 中所有的xy都需要计算得出 currentPosToFragCoordDir ,从而计算当前的 projectedDist
// 2、通过计算 currentPosToFragCoordDir 在 -velocityDir 的投影长度,得到 >0 和 <= 0 两个结果
// 如果是 >0 ,即夹角小于90°,则表明了当前的像素在雨滴后方(轨迹线区域),此时雨滴已经经过了
// 如果是 <=0 ,即夹角小于等于90°,则表明了当前的像素在雨滴前方或与雨滴位置重合(不显示轨迹线),此时雨滴还没有进入
// 投影的长度,值越大,像素距离雨滴越远,轨迹线透明度越高(可以通过 smoothstep 实现淡出效果)
// ------------------------------------------------------------------------------------------------------------------
// | 这条虚线是 velocityDir ,方向是斜向右下
// | \ · 像素A的 fragCoord.xy 计算 A 的投影长度,首先 velocityDir 反方向,得到斜向左上
// | \ 然后 fragCoord - currentPos 得到从 pos 指向 A 的向量
// | \ 最后计算dot,此时可以看到 结果 >0 ,因为夹角是小于90°
// | * 这个是当前的 pos.xy
// | \ · 像素B的 fragCoord.xy 计算 B 的投影长度,首先 velocityDir 反方向,得到斜向左上
// | \ 然后 fragCoord - currentPos 得到从 pos 指向 B 的向量
// | \ 最后计算dot,此时可以看到 结果 <0 ,因为夹角是大于90°
// |____________________________
// ------------------------------------------------------------------------------------------------------------------
// 计算当前像素位置与雨滴位置之间的向量差。这个向量表示从雨滴位置指向当前像素的方向
vec2 currentPosToFragCoordDir = fragCoord - currentPos;
// 计算当前像素在雨滴运动方向上的投影距离
// dot 函数计算两个向量的点积,结果是一个标量,表示当前像素在雨滴运动方向上的投影长度。
// -velocityDir 是雨滴运动方向的反方向,因为雨滴是从上方落下,所需要的投影方向是从雨滴位置指向像素位置
float projectedDist = dot(currentPosToFragCoordDir, -velocityDir);
// 通过上面两行,计算 fragCoord.xy 在雨滴后方,还是前方
// 接下来,开始计算切向距离的平方(tanjential distance squared)
// 切向距离的平方,是描述一个点到一条直线的垂直距离的平方
// 为什么要计算切向距离的平方?1、判断点是否在轨迹宽度内;2、用于生成虚线效果;3、用于生成渐变效果
// 计算步骤:
// 1、计算从点 pos 到点 fragCoord 的向量 currentPosToFragCoordDir(currentPosToFragCoordDir = fragCoord - currentPos)
// 2、计算 currentPosToFragCoordDir 在 velocity_dir 方向上的投影距离 projectedDist(dot(currentPosToFragCoordDir, -velocityDir))
// 3、计算 currentPosToFragCoordDir 的模的平方(dot(currentPosToFragCoordDir, currentPosToFragCoordDir))
// 4、利用勾股定理,计算切向距离的平方
float tanjentialDistanceSquared = dot(currentPosToFragCoordDir, currentPosToFragCoordDir) - pow(projectedDist, 2.0);
// 由于是平方,所以size也要平方
float sizeSquared = pow(rainSize, 2.0);
// 两个计算步骤
// 使用 step 和 smoothstep 函数来确定点 fragCoord.xy 是否在直线的 size 范围内
// -----------------------------------------------------------------------------
// 1、
// 当 projectedDist < 0.0,返回0,否则返回1
// 作用:判断点是否在轨迹的投影方向上
// -----------------------------------------------------------------------------
// 2、
// 由于 edge0 < edge1,当 tanjentialDistanceSquared < sizeSquared / 2.0,返回0;当 tanjentialDistanceSquared > sizeSquared,返回1
// 作用:生成一个从轨迹中心向外逐渐衰减的效果
// 如果 tanjentialDistanceSquared 接近轨迹中心(即较小的值), smoothstep 返回接近 0 的值
// 如果 tanjentialDistanceSquared 接近轨迹的边缘(即较大的值), smoothstep 返回接近 1 的值
// 使用 1.0 - smoothstep(...) 的原因是为了反转 smoothstep 的效果
float line = step(0.0, projectedDist) * (1.0 - smoothstep(sizeSquared / 2.0, sizeSquared, tanjentialDistanceSquared));
// 计算原理是通过结合直线的宽度范围和周期性的余弦函数来创建一个虚线效果
// cos(0.3 * projectedDist - PI / 3.0):计算一个周期性的余弦函数,其周期由 0.3 决定,相位由 -PI / 3.0 决定
// 这个余弦函数的值在 -1 到 1 之间变化
// step(0.5, cos(0.3 * projectedDist - PI / 3.0)):使用 step 函数将余弦函数的值转换为二值
// 当余弦函数的值大于 0.5 时为 1,否则为 0。这创建了一个虚线的模式,其中 1 表示虚线的实线部分,0 表示虚线的空隙部分
// <<< 可以使用sin,不使用cos >>>
// 将 line 与虚线模式相乘,得到最终的虚线效果
// 只有当点 fragCoord.xy 在直线的 size 范围内且在虚线的实线部分时, dashedLine 才为 1,否则为 0
float dashedLine = line * step(0.5, cos(0.3 * projectedDist - PI / 3.0));
// 目的是为虚线添加一个渐隐效果,使其在远离起点 pos 的方向上逐渐变淡
// projectedDist:这是点 fragCoord.xy 在 -velocityDir 方向上(即沿着虚线的方向)的投影距离
// rainLen / 5.0 和 rainLen:这两个值定义了一个渐变范围。rainLen / 5.0 是渐变的起始点,rainLen 是渐变的结束点。
// smoothstep 函数:是一个平滑的阶梯函数,用于在两个值之间进行平滑过渡。其定义如下:
// 因此,smoothstep(rainLen / 5.0, rainLen, projected_dist) 的作用是:
// 当点 fragCoord.xy 靠近起点 pos(即 projected_dist 小于 rainLen / 5.0)时,返回接近 0 的值
// 当点 fragCoord.xy 远离起点 pos(即 projected_dist 大于 rainLen)时,返回接近 1 的值
// 在中间区域,返回一个平滑过渡的值
float fadingDashedLine = dashedLine * (1.0 - smoothstep(rainLen / 5.0, rainLen, projectedDist));
return vec3(fadingDashedLine);
}
vec3 drawDiffusionWave(vec2 endPos, vec2 fragCoord, float time) {
// waveSize 扩散波由小变大得更加明显
float waveSize = 10.0;
float innerRadius = (0.05 + 0.8 * time) * waveSize;
float outerRadius = innerRadius + 0.25;
// 这两行这样计算的原因
// 在上面设置了内圈和外圈,那么接下来就需要知道整个屏幕中所有 fragCoord.xy 和 endPos 的距离
// 只有符合的距离(即下面的 ring 的计算),才能渲染到屏幕中
// ------------------------------------------------------------------------------------------------------------------
// | 这条虚线是 velocityDir ,方向是斜向右下
// | \
// | \ · 像素A的 fragCoord.xy
// | \
// | \ · 像素B的 fragCoord.xy
// | \
// | \ · 像素C的 fragCoord.xy
// | \
// | * 这个是结束的 pos.xy
// | ( (------)·)像素D的 fragCoord.xy 此时只有像素D符合 ring 的计算范围内
// | 这里是 wave 有内圈和外圈
// |____________________________
// ------------------------------------------------------------------------------------------------------------------
vec2 endPosToFragCoordDir = fragCoord - endPos;
// 进行 * vec2(1.0, 3.0) 操作,是为了把Y轴给压扁
// 比如期望得到的结果是1.0,此时y的值只需要是0.5即可
float distortedDist = length(endPosToFragCoordDir * vec2(1.0, 3.0));
// 假设 innerRadius 是0.5,那么 outerRadius 是0.75
// smoothstep(0.5, 0.5 + 5.0, distortedDist) 小于0.5的位置都0.0(消失),0.5 ~ 5.5之间的渐变,大于5.5的位置都是1.0
// smoothstep(0.75, 0.75 + 5.0, distortedDist) 小于0.75的位置都0.0(消失),0.75 ~ 5.75之间的渐变,大于5.75的位置都是1.0
// 有个 1-,所以大于5.75的位置都是0.0(消失),小于0.75的位置都是1.0
// 两个相乘,0 ~ 0.5,是0.0;0.5 ~ 0.75是渐变A,0.75 ~ 5.5是渐变B,5.0 ~ 5.75是渐变C,大于5.75是0.0
float ring = smoothstep(innerRadius, innerRadius + 5.0, distortedDist) * (1.0 - smoothstep(outerRadius, outerRadius + 5.0, distortedDist));
// elapsedTime - fallTime 是正数或者0,所以smoothstep的范围是 0 ~ 1
// 1- 的作用就是让 ring 逐渐消失
float fadingRing = ring * (1.0 - smoothstep(0.0, 0.7, time));
return vec3(fadingRing);
}
vec3 getWaterPlaneColor(vec3 p) {
vec3 color = vec3(0.0, 0.0, 0.0);
return color;
}
// 计算一个点 p 到一个无限大水平平面(地面)的距离
// 这个平面通常被定义为 y=0 的平面,即地面位于 y 轴的零点
float getWaterPlaneDist(vec3 p) {
float planeY = p.y;
// 添加一个小的偏移量防止除零
float epsilon = 0.0001;
// 使用更稳定的水面距离计算
return p.y + epsilon; // 保持简单但稳定的计算
}
// 取物体距离相机最近的 dist
vec2 getDist(vec3 pos) {
float waterPlaneDist = getWaterPlaneDist(pos);
// 使用数组和循环找到最小距离
float distances[1];
distances[0] = waterPlaneDist;
// 使用数组和循环找到对应的物体类型
int objectTypes[1];
objectTypes[0] = WATER_PLANE;
float minDist = distances[0];
int objectType = objectTypes[0];
return vec2(minDist, float(objectType));
}
// 在基于距离场(Distance Field)的渲染中,法线(Normal)的计算是一个关键步骤
// 距离场是一种标量场,其中每个点的值表示该点到最近表面的距离
// 由于距离场本身是一个标量函数,而法线是一个向量,因此需要通过某种方式从标量场中提取出向量信息
// 有限差分法(Finite Difference Method)是一种自然且有效的方法,用于从标量场中近似计算梯度,进而得到法线
// 对于 x 方向
// grad_x ≈ [f(p + eps_x) - f(p - eps_x)] / (2 * eps) * ex
// 具体查看(自定义效果 - 两个物体的合并 -1.png)、(自定义效果 - 两个物体的合并 -2.png)、(自定义效果 - 两个物体的合并 -3.png)
vec3 getNormal(vec3 p) {
float e = 0.001;
// vec3(..., 0.0, 0.0):构造一个三维向量,只有 x 分量有效
// 通过对点 p 在 x 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 x 方向上的变化率,可以得到 x 方向上的梯度
// 结果是一个三维向量,但只有 x 分量是非零的,表示梯度在 x 方向上的分量
float x1 = getDist(p + vec3(e, 0.0, 0.0)).x;
float x2 = getDist(p - vec3(e, 0.0, 0.0)).x;
vec3 grad_x = vec3((x1 - x2) / (2.0 * e), 0.0, 0.0);
// vec3(0.0, ..., 0.0):构造一个三维向量,只有 y 分量有效
// 通过对点 p 在 y 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 y 方向上的变化率,可以得到 y 方向上的梯度
// 结果是一个三维向量,但只有 y 分量是非零的,表示梯度在 y 方向上的分量
float y1 = getDist(p + vec3(0.0, e, 0.0)).x;
float y2 = getDist(p - vec3(0.0, e, 0.0)).x;
vec3 grad_y = vec3(0.0, (y1 - y2) / (2.0 * e), 0.0);
// vec3(0.0, 0.0, ...):构造一个三维向量,只有 z 分量有效
// 通过对点 p 在 z 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 z 方向上的变化率,可以得到 z 方向上的梯度
// 结果是一个三维向量,但只有 z 分量是非零的,表示梯度在 z 方向上的分量
float z1 = getDist(p + vec3(0.0, 0.0, e)).x;
float z2 = getDist(p - vec3(0.0, 0.0, e)).x;
vec3 grad_z = vec3(0.0, 0.0, (z1 - z2) / (2.0 * e));
// 将三个方向上的梯度向量相加,得到该点的总梯度向量
// 然后使用 normalize 函数将其标准化(即长度变为 1),得到法向量
// 因为梯度向量指向函数值增长最快的方向,而对于表示表面的函数,其法向量与梯度向量方向相反(或相同,取决于表面函数的定义,但通常通过取梯度来近似法向量,并可能需要根据具体情况调整方向)
// 标准化确保了返回的法向量是一个单位向量
// return normalize(grad_x + grad_y + grad_z);
vec3 normal = normalize(grad_x + grad_y + grad_z);
return normal;
}
// 左手坐标系
// ro:相机的位置(Ray Origin),即相机在三维空间中的坐标。这个点是所有视线(或光线)的起点
// target:相机的目标点,即相机“看”向的点。这个点决定了相机的前进方向(Forward 向量)
// up:相机的向上方向(Up 向量),通常与相机的前进方向垂直,这个向量用于确定相机的右侧方向(Right 向量)和确保相机的坐标系是正交的
// 相机源点、目标、向上方向
// R、U、F 分别是 Right、Up 和 Forward 向量
mat3 getCameraMat(vec3 ro, vec3 target, vec3 up) {
vec3 f = normalize(target - ro); // 计算 Forward 向量(F)
// 叉积 cross(a, b) 的结果是一个垂直于向量 a 和 b 的向量
// 注意:由于使用的是左手坐标系,所以是使用 up 叉乘 f,而不是反过来进行叉乘,进行叉乘运算时一定要注意其方向性!
vec3 r = cross(up, f); // Right 向量(R)是 Forward 向量和 Up 向量的叉积,表示相机的右侧方向
vec3 u = normalize(cross(f, r)); // 为了确保 Up 向量垂直于 Forward 向量,需要重新计算 Up 向量为 Right 向量和 Forward 向量的叉积
return mat3(r, u, f);
}
// rayOrigin 代表视线(或光线)的起点
// rayDirection 代表视线(或光线)的方向
vec2 rayMarching(vec3 rayOrigin, vec3 rayDirection) {
// disTotal 是视线(或光线)从 rayOrigin 出发后行进的总距离
float disTotal = 0.0;
const float MAX_STEPS = 100.0;
float SURFACE_DIST = 0.0001;
float objectType = -1.0;
for (float i = 0.0; i < MAX_STEPS; i += 1.0) {
// 视线(或光线)的当前位置 pos
// rayDirection * disTotal 计算视线(或光线)在方向 rayDirection 上行进距离 disTotal 后的向量,然后将这个向量加到源点 rayOrigin 上,得到新的位置 pos。
vec3 pos = rayOrigin + disTotal * rayDirection;
// dS 表示从当前视线(或光线)位置 p 到最近的场景物体表面的距离
float dS = getDist(pos).x;
disTotal += dS;
// 射中的物体类型
objectType = getDist(pos).y;
// 如果从当前位置到球面的距离 dS 小于某个阈值 SURFACE_DIST,则可能表示视线(或光线)已经“击中”了表面,因此退出循环
// 因为点 p 可能在物体内,float dS = getDist(pos).x 中的 dS 会返回负值
// 如果视线(或光线)行进的距离 disTotal 大于某个最大距离 MAX_DIST,则退出循环,可能是因为视线(或光线)已经行进得太远而没有“击中”任何物体
if (dS < SURFACE_DIST || disTotal > MAX_DIST) {
break;
}
}
// 返回是否命中的标志,未命中的,则是天空色
bool hit = disTotal < MAX_DIST;
return vec2(disTotal, hit ? objectType : -1.0);
}
// 计算一个 3D 点 p 与光源之间的漫反射光照强度
float getLightDif(vec3 lightPos, vec3 p) {
float SHADOW = 0.1;
// 计算从点 p 到光源的方向,方向的箭头指向 lightPos
// 首先计算从点 p 到光源 lightPos 的向量
// 然后,使用 normalize 函数将这个向量标准化(或归一化),使其长度为 1
// 标准化后的向量 l 表示,从点 p 指向光源的方向
vec3 l = normalize(lightPos - p);
vec3 n = getNormal(p);
// 计算漫反射光照强度
// 使用点积(dot 函数)来计算法线向量 n 和光源方向向量 l 之间的角度的余弦值
// 这个余弦值表示了光源方向和表面法线之间的“对齐”程度,从而决定了光照的强度
// 由于余弦值可能是负的(当光源在表面的背面时),使用 max 函数确保结果始终是非负的
// 因此,dif 变量存储了漫反射光照的强度
float dif = max(dot(n, l), 0.0);
// 如果不需要阴影
// return dif;
// 阴影检测(偏移起点防止自相交)
float dis = rayMarching(p + 0.01 * n, l).x;
if (dis < length(lightPos - p)) {
// 存在遮挡则减弱光照
dif *= SHADOW;
}
return dif;
}
// 根据给定的方向向量 eye 计算天空的颜色这个函数模拟了天空的渐变效果,从地平线到天顶的颜色变化
vec3 getSkyColor(vec3 eye) {
// 方案1 -------------------------------------------------------------------
// max(e.y, 0.0):确保 e.y 的值不小于 0,因为天空的颜色通常在地平线以上
// max(e.y, 0.0) * 0.8:将 e.y 的值乘以 0.8,使颜色变化更加平缓
// max(e.y, 0.0) * 0.8 + 0.2:在乘以 0.8 的基础上加上 0.2,确保地平线处的颜色不会太暗
// (max(e.y, 0.0) * 0.8 + 0.2) * 0.8:再次乘以 0.8,进一步调整颜色的亮度
eye.y = (max(eye.y, 0.0) * 0.8 + 0.2) * 0.8;
// pow(1.0 - eye.y, 2.0):计算 1.0 - eye.y 的平方,用于模拟天空颜色的非线性变化这个值将用于红色分量,使天空在地平线处更红
// 1.0 - eye.y:直接使用 1.0 - eye.y 作为绿色分量,使天空在地平线处更绿
// 0.6 + (1.0 - eye.y) * 0.4:计算蓝色分量,使天空在地平线处更蓝 0.6 是基础蓝色,(1.0 - eye.y) * 0.4 根据 eye.y 的值增加蓝色
// vec3(pow(1.0 - eye.y, 2.0), 1.0 - eye.y, 0.6 + (1.0 - eye.y) * 0.4):将计算得到的红、绿、蓝分量组合成一个颜色向量
// vec3(pow(1.0 - eye.y, 2.0), 1.0 - eye.y, 0.6 + (1.0 - eye.y) * 0.4) * 1.1:将颜色向量乘以 1.1,增加颜色的亮度
return vec3(pow(1.0 - eye.y, 2.0), 1.0 - eye.y, 0.6 + (1.0 - eye.y) * 0.4) * 1.1;
// 方案2 -------------------------------------------------------------------
// // 归一化 eye.y 到 [0, 1] 范围
// eye.y = (eye.y + 1.0) * 0.5;
// // 使用更平滑的渐变公式
// float t = clamp(eye.y * 0.5 + 0.5, 0.0, 1.0);
// vec3 skyTop = vec3(0.2, 0.5, 1.0); // 天顶颜色(亮蓝色)
// vec3 skyHorizon = vec3(0.8, 0.8, 0.9); // 地平线颜色(亮白色)
// // 使用平滑过渡
// return mix(skyHorizon, skyTop, pow(t, 0.5));
}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
// 归一化 uv 的坐标范围到 [-1, 1]
// [0, 1] -> [-0.5, 0.5]<* 0.5>
vec2 uv = (fragCoord.xy - u_resolution.xy * 0.5) / min(u_resolution.y, u_resolution.x);
vec3 finalColor = vec3(0.0);
// 相机信息
vec3 cameraPos = vec3(0.0, 2.0, -8.0);
vec3 cameraUp = vec3(0.0, 1.0, 0.0);
vec3 cameraTarget = vec3(0.0, 0.0, 0.0);
// 光源的位置
vec3 lightPos = vec3(3.0 + sin(u_time), 8.0, -5.0 + cos(u_time));
vec3 lightColor = vec3(1.0, 1.0, 1.0);
// rayDirection 是视线(或光线)的方向向量,表示视线(或光线)沿着哪个方向行进
// 之所以 * vec3(uv, 1.0),uv.x 表示水平方向的偏移,uv.y 表示垂直方向的偏移,1.0 表示沿着相机的前进方向(即深度方向)的偏移
// 1.0 表示沿着相机的前进方向(即深度方向)的偏移
// vec3(uv, 1.0) 将二维的 uv 坐标扩展为一个三维向量,其中 z 分量为 1.0
vec3 rayDirection = getCameraMat(cameraPos, cameraTarget, cameraUp) * vec3(uv, 1.0);
// 射中物体的结果
vec2 rayResult = rayMarching(cameraPos, rayDirection);
// 视线(或光线)步进行进了多少距离
float rayDist = rayResult.x;
// 射中的当前的物体
float objectType = rayResult.y;
// 视线(或光线)的当前位置 p(从相机发出一条射线,一直延伸到接触了物体表面,这之间的距离)
// rayDirection * rayDist 计算视线(或光线在方向 rayDirection 上行进距离 rayDist 后的向量,然后将这个向量加到源点 cameraTarget 上,得到新的位置 p。
vec3 pointOfCameraTouchObject = cameraPos + rayDirection * rayDist;
// 点 p 与光源之间的漫反射光照强度
float lightDif = getLightDif(lightPos, pointOfCameraTouchObject);
vec3 waterPlaneColor = getWaterPlaneColor(pointOfCameraTouchObject);
// 获取物体颜色
vec3 objectColor = vec3(0.0);
if(int(objectType) == -1) { // 如果没有碰到物体或者水面,使用天空颜色
finalColor = getSkyColor(normalize(rayDirection)); // 直接使用天空色
} else if(int(objectType) == 0) { // 碰撞到水面,对水面进行反射计算
// 1、水面的法线
// 水面法线的计算基于距离场的梯度,使用有限差分法近似计算
// ∇f(p) ≈ (f(p + ϵ) - f(p - ϵ)) / 2ϵ f(p) 是距离场函数,表示点 p 到最近表面的距离。ϵ 是一个小的偏移量,用于数值计算
// 2、反射光线方向
// 反射定律描述了光线在光滑表面上的反射行为,即入射角等于反射角
// 公式是 reflectDir = rayDirection - 2 * (rayDirection ⋅ waterNormal) * waterNormal
// 3、反射光线的追踪
// 4、菲涅尔效应
// 菲涅尔效应描述了光线在不同入射角下的反射强度变化
// 公式是 R(θ)=R0 + (1 - R0) * (1 - cosθ)^5
// R(θ) 是反射率
// R0是法线方向上的反射率,通常为((n1 - n2) / (n1 + n2))^2,其中 n1和 n2分别是两种介质的折射率
// θ 是入射角
// 改案例简化了菲涅尔公式,使用了 (1 - cosθ)^2来近似反射率的变化,并通过 clamp 函数限制了反射强度的范围
// 5、光照模型
// 动态水面法线(这里可以添加波纹噪声计算)
vec3 waterNormal = normalize(getNormal(pointOfCameraTouchObject));
// 调整水面法线以创建波纹效果
float waveHeight = 0.05; // 波纹高度
float waveFreq = 1.0; // 波纹频率
waterNormal.y += sin(pointOfCameraTouchObject.x * waveFreq + u_time * 0.5) * waveHeight * 0.5;
waterNormal.y += sin(pointOfCameraTouchObject.z * waveFreq * 0.5 + u_time * 0.5) * waveHeight * 0.5;
waterNormal = normalize(waterNormal);
// 反射光线方向
vec3 reflectDir = reflect(rayDirection, waterNormal);
// 反射光线的追踪
vec2 reflectResult = rayMarching(pointOfCameraTouchObject + waterNormal * 0.01, reflectDir);
// 反射点的颜色
vec3 reflectColor = vec3(0.0);
// 天空反射
reflectColor = getSkyColor(normalize(reflectDir));
// 水面基础颜色
vec3 waterColor = getWaterPlaneColor(pointOfCameraTouchObject);
// 菲涅尔效应(根据视角调整反射强度)
float fresnel = pow(1.0 - abs(dot(rayDirection, waterNormal)), 2.0);
fresnel = clamp(fresnel, 0.3, 0.9); // 限制反射强度范围
// 混合反射颜色和水面颜色
vec3 finalReflectColor = mix(waterColor, reflectColor, fresnel);
// 添加环境光
float ambient = 0.2;
objectColor = finalReflectColor * (lightDif + ambient);
finalColor = objectColor * lightColor * max(lightDif, 0.1); // 确保最小光照强度;
} else { // 碰撞到物体
// objectColor = getObjectColor(int(objectType));
// 计算最终颜色(物体颜色 * 光照颜色 * 光照强度)
// finalColor = objectColor * lightColor * lightDif;
}
float rainSize = 1.0;
float rainLen = 70.0;
float fallTime = 0.7; // 所有雨滴最多可以存在的时间,固定为0.7或者其他正数。
float lifeTime = fallTime * 2.0;
// 必须要乘以 u_resolution.y
// 如果不乘以 u_resolution.y,雨滴的垂直速度将是一个固定的值(例如 -0.9),而与屏幕高度无关
// 在高分辨率屏幕上(例如 1920x1080),雨滴的垂直速度会显得非常小,因为屏幕高度较大,雨滴需要很长时间才能从屏幕顶部移动到底部。这会导致雨滴看起来几乎不动,或者移动得非常缓慢,无法产生明显的下落效果
// 在低分辨率屏幕上(例如 320x240),雨滴的垂直速度会显得过大,因为屏幕高度较小,雨滴会瞬间从屏幕顶部移动到底部,导致雨滴看起来像是瞬间消失,而不是自然地下落
// 通过将速度与屏幕高度成比例(即乘以 u_resolution.y),可以确保雨滴在不同分辨率的屏幕上都能以类似的时间从顶部移动到底部
// -0.5 * u_resolution.x ,表示雨滴在水平方向上的速度分量
// -0.8 * u_resolution.y ,表示雨滴在垂直方向上的速度分量
vec2 velocity = vec2(-0.5 * u_resolution.x, -0.8 * u_resolution.y) / fallTime;
vec2 velocityDir = normalize(velocity);
for(float i = 0.0; i < rainNum; i+= 1.0) {
float time = u_time + lifeTime * (i + i / rainNum); // time需要逐渐增大
float elapsedTime = mod(time, lifeTime); // 当前已过的时间,范围是 0 ~ fallTime
float val = i + floor(time / lifeTime - i) * rainNum;
vec2 pos = randomRainPos(val, u_resolution.xy, velocity);
if(elapsedTime < fallTime) { // 如果还没到地面(消失)的时刻
vec2 currentPos = pos + velocity * elapsedTime;
finalColor += drawRain(rainSize, rainLen, currentPos, fragCoord, velocityDir);
} else {
vec2 endPos = pos + velocity * fallTime;
finalColor += drawDiffusionWave(endPos, fragCoord, elapsedTime - fallTime);
}
}
gl_FragColor = vec4(finalColor, 1.0);
}
`)
})
}
</script>
物体跳动、天空云朵、山 (未完成)
点击运行
getObjectY要使用fract是为了和heightWave的时间一致
https://juejin.cn/post/7393533296242114598
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<div>getObjectY要使用fract是为了和heightWave的时间一致</div>
<div style="opacity: 0;">https://juejin.cn/post/7393533296242114598</div>
<canvas v-if="isRunning" id="animalJump" class="shader-toy-stage bg-black"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted } from 'vue'
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onStart()
} else {
isRunning.value = false
}
}
onMounted(async () => {
await nextTick()
// isRunning.value = true
// await nextTick()
// onStart()
})
const onStart = () => {
import('glslCanvas').then(module => {
const canvas = document.getElementById('animalJump')
const glslCanvas: any = new module.default(canvas)
glslCanvas.load(`
#extension GL_OES_standard_derivatives: enable
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
const float MAX_DIST = 1000.0;
const float JUMP_SPEED = 4.0;
const float MOVE_SPEED = 0.0;
const float PI = 3.1415926;
const float IS_LESS_THAN_GROUND_Y = 0.1;
// 同步跳动和地面下塌的时间
float sameTime() {
return fract(u_time);
}
// 物体跳动时候的y值
float getObjectY() {
float t = sameTime();
return sin(t * JUMP_SPEED);
}
// sky
// 根据给定的方向向量 e 计算天空的颜色这个函数模拟了天空的渐变效果,从地平线到天顶的颜色变化
vec3 getSkyColor(vec3 e) {
// max(e.y, 0.0):确保 e.y 的值不小于 0,因为天空的颜色通常在地平线以上
// max(e.y, 0.0) * 0.8:将 e.y 的值乘以 0.8,使颜色变化更加平缓
// max(e.y, 0.0) * 0.8 + 0.2:在乘以 0.8 的基础上加上 0.2,确保地平线处的颜色不会太暗
// (max(e.y, 0.0) * 0.8 + 0.2) * 0.8:再次乘以 0.8,进一步调整颜色的亮度
e.y = (max(e.y, 0.0) * 0.8 + 0.2) * 0.8;
// pow(1.0 - e.y, 2.0):计算 1.0 - e.y 的平方,用于模拟天空颜色的非线性变化这个值将用于红色分量,使天空在地平线处更红
// 1.0 - e.y:直接使用 1.0 - e.y 作为绿色分量,使天空在地平线处更绿
// 0.6 + (1.0 - e.y) * 0.4:计算蓝色分量,使天空在地平线处更蓝 0.6 是基础蓝色,(1.0 - e.y) * 0.4 根据 e.y 的值增加蓝色
// vec3(pow(1.0 - e.y, 2.0), 1.0 - e.y, 0.6 + (1.0 - e.y) * 0.4):将计算得到的红、绿、蓝分量组合成一个颜色向量
// vec3(pow(1.0 - e.y, 2.0), 1.0 - e.y, 0.6 + (1.0 - e.y) * 0.4) * 1.1:将颜色向量乘以 1.1,增加颜色的亮度
return vec3(pow(1.0 - e.y, 2.0), 1.0 - e.y, 0.6 + (1.0 - e.y) * 0.4) * 1.1;
}
// 黑白网格
vec3 groundGrid(vec3 p) {
vec3 c1 = vec3(1.0);
vec3 c2 = vec3(0.0);
// 这段函数,远处的格子会变形
// floor(p.x) 和 floor(p.z) 分别取 p.x 和 p.z 的整数部分
// mod(..., 2.0) 取上述和对 2 的模,结果为 0 或 1
float s = mod(floor(p.x) + floor(p.z), 2.0);
// 调整网格的大小,使其在远处更小
// float scale = 1.0 + abs(p.z) * 0.1; // 根据 z 距离调整网格大小
// float s = mod(floor(p.x / scale + floor(p.z / scale)), 2.0);
// mix(c1, c2, s) 根据 s 的值在 c1 和 c2 之间进行插值
// 当 s 为 0 时,返回 c1(白色)
// 当 s 为 1 时,返回 c2(黑色)
vec3 groundColor = mix(c1, c2, s);
return groundColor;
}
// 计算一个点 p 到一个无限大水平平面(地面)的距离
// 这个平面通常被定义为 y=0 的平面,即地面位于 y 轴的零点
float getGroundDist(vec3 p) {
float groundY = p.y;
return groundY;
}
// 计算一个点 p 到一个球体的距离
// 球体的中心由 sphere 表示,半径由 radius 表示
float getSphereDist(vec3 p) {
float y = sin(u_time * JUMP_SPEED) + 2.0;
float z = 0.0 - u_time * MOVE_SPEED;
vec3 spherePos = vec3(0.0, y , z);
float radius = 1.0;
// 从当前点的位置 p 到球体中心的距离中减去球体的半径,得到点 p 到球体表面的最短距离
// 如果点 p 在球体内部,sphereDist 为负值
// 如果点 p 在球体表面,sphereDist 为零
// 如果点 p 在球体外部,sphereDist 为正值
float sphereDist = length(p - spherePos) - radius;
return sphereDist;
}
// 计算一个点 p 到一个立方体的(最近的)距离,要考虑 p 在内部和外部的情况,二者都要计算得出结果,外部返回正值,内部返回负值
// 假设立方体的半尺寸为 cubeSize = vec3(1.0, 1.0, 1.0),表示立方体在 x、y、z 轴上的半长度都为 1.0,立方体的中心位于原点 (0, 0, 0)
// 如果点 p = vec3(1.5, 1.5, 1.5),则 p 在立方体外部
// 如果点 p = vec3(0.5, 0.5, 0.5),则 p 在立方体内部
// 如果点 p = vec3(1.0, 1.0, 1.0),则 p 在立方体边界上
// 通过 getCubeDist 函数,可以计算出点 p 到立方体的最短距离
float getCubeDist(vec3 p) {
float size = 0.8;
float y = getObjectY() + 1.0 + size;
float z = 0.0 - u_time * MOVE_SPEED;
vec3 cubePos = vec3(0.0, y, z);
// 改变高度,0.8->0.5->0.8......
float h = 0.5 + 0.3 * step(0.8, y);
vec3 cubeSize = vec3(size, size, size);
// 旋转矩阵,围绕 y 轴旋转
// 将 u_time 映射到 -30 到 30 度
float angle = sin(u_time * JUMP_SPEED / 2.0) * 30.0;
angle = radians(angle); // 将角度转换为弧度
// x 第一列 y 第二列 z 第三列
mat3 rotationMatrix = mat3(
cos(angle), 0.0, sin(angle),
0.0, 1.0, 0.0,
-sin(angle), 0.0, cos(angle)
);
// 应用旋转
vec3 rotatedP = rotationMatrix * (p - cubePos) + cubePos;
// 应用旋转
vec3 tempDist = abs(rotatedP - cubePos) - cubeSize;
// abs(p - cubePos) - cubeSize 计算点 p 相对于立方体中心 cubePos 的距离
// 首先计算点 p 与立方体中心 c 之间的差值 p − cubePos
// 然后取绝对值 abs(p − cubePos),表示点 p 相对于立方体中心的水平、垂直和深度方向的距离
// 最后减去立方体的半尺寸 cubeSize,得到 tempDist
// tempDist 的每个分量表示点在对应方向上超出立方体边界的距离
// 点在立方体内部,所有值是负值
// 点在立方体边界,至少一个是 0
// 点在立方体外,至少一个是正值
// vec3 tempDist = abs(p - cubePos) - cubeSize;
// max(tempDist, 0.0) 将 tempDist 的所有负分量设置为 0,只保留正分量,表示点到立方体外部的距离(假如其中有一个是负值,则 p 是在立方体内)
// length(max(tempDist, 0.0)) 计算这个向量的长度,即点到立方体外部的最短距离
// -----------------------------------------------------------------------
// min(max(tempDist.x, max(tempDist.y, tempDist.z)), 0.0) 这部分的作用是处理点在立方体内部的情况
// max(tempDist.x, max(tempDist.y, tempDist.z)) 找出 tempDist 中最大的分量,表示点在立方体内部最深的轴向距离
// min(..., 0.0) 确保这个值不会超过 0,因为点在立方体内部时,tempDist 的所有分量都是负值
// min(max(tempDist.x, tempDist.y, tempDist.z), 0.0) 将这个最大值与 0 比较,取较小值,表示点距离立方体在某个轴上最近的面的距离为(x)个单位
// -----------------------------------------------------------------------
// 最后合并距离,将点到立方体外部的距离和点到立方体内部的距离相加,得到点到立方体的最短距离
// -----------------------------------------------------------------------
// 假如点在外部,则会使用 length(max(tempDist, 0.0))
// 假如点在内部,则会使用 min(max(tempDist.x, max(tempDist.y, tempDist.z)), 0.0)
float cubeDist = length(max(tempDist, 0.0)) + min(max(tempDist.x, max(tempDist.y, tempDist.z)), 0.0);
return cubeDist;
}
// 扩散波,控制产生扩散波的周期
float heightWave(vec3 pos) {
float groundWave = 0.0;
float time = sameTime();
float len = length(pos.xz);
float tt = time * 15.0 - PI * 2.0 - len * 3.0;
groundWave = 0.1 * exp(-len * len) * sin(tt) * exp(-max(tt, 0.0) / 2.0) * smoothstep(0.0, 0.01, time);
return groundWave;
}
// 取物体距离相机最近的 dist
float getDist(vec3 pos) {
float sphereDist = getSphereDist(pos);
float cubeDist = getCubeDist(pos);
float groundDist = getGroundDist(pos);
// 扩散波
float wave = heightWave(pos);
groundDist = groundDist - wave;
// return min(groundDist, min(cubeDist, sphereDist));
return min(groundDist, cubeDist);
}
// 在基于距离场(Distance Field)的渲染中,法线(Normal)的计算是一个关键步骤
// 距离场是一种标量场,其中每个点的值表示该点到最近表面的距离
// 由于距离场本身是一个标量函数,而法线是一个向量,因此需要通过某种方式从标量场中提取出向量信息
// 有限差分法(Finite Difference Method)是一种自然且有效的方法,用于从标量场中近似计算梯度,进而得到法线
// 对于 x 方向
// grad_x ≈ [f(p + eps_x) - f(p - eps_x)] / (2 * eps) * ex
// 具体查看(自定义效果 - 两个物体的合并 -1.png)、(自定义效果 - 两个物体的合并 -2.png)、(自定义效果 - 两个物体的合并 -3.png)
vec3 getNormal(vec3 p) {
float e = 0.001;
// vec3(..., 0.0, 0.0):构造一个三维向量,只有 x 分量有效
// 通过对点 p 在 x 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 x 方向上的变化率,可以得到 x 方向上的梯度
// 结果是一个三维向量,但只有 x 分量是非零的,表示梯度在 x 方向上的分量
vec3 grad_x = vec3((getDist(p + vec3(e, 0.0, 0.0)) - getDist(p - vec3(e, 0.0, 0.0))) / (2.0 * e), 0.0, 0.0);
// vec3(0.0, ..., 0.0):构造一个三维向量,只有 y 分量有效
// 通过对点 p 在 y 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 y 方向上的变化率,可以得到 y 方向上的梯度
// 结果是一个三维向量,但只有 y 分量是非零的,表示梯度在 y 方向上的分量
vec3 grad_y = vec3(0.0, (getDist(p + vec3(0.0, e, 0.0)) - getDist(p - vec3(0.0, e, 0.0))) / (2.0 * e), 0.0);
// vec3(0.0, 0.0, ...):构造一个三维向量,只有 z 分量有效
// 通过对点 p 在 z 方向上加上和减去 e,然后计算距离差,这个差值反映了距离场在 z 方向上的变化率,可以得到 z 方向上的梯度
// 结果是一个三维向量,但只有 z 分量是非零的,表示梯度在 z 方向上的分量
vec3 grad_z = vec3(0.0, 0.0, (getDist(p + vec3(0.0, 0.0, e)) - getDist(p - vec3(0.0, 0.0, e))) / (2.0 * e));
// 将三个方向上的梯度向量相加,得到该点的总梯度向量
// 然后使用 normalize 函数将其标准化(即长度变为 1),得到法向量
// 因为梯度向量指向函数值增长最快的方向,而对于表示表面的函数,其法向量与梯度向量方向相反(或相同,取决于表面函数的定义,但通常通过取梯度来近似法向量,并可能需要根据具体情况调整方向)
// 标准化确保了返回的法向量是一个单位向量
return normalize(grad_x + grad_y + grad_z);
}
// 左手坐标系
// ro:相机的位置(Ray Origin),即相机在三维空间中的坐标。这个点是所有视线(或光线)的起点
// target:相机的目标点,即相机“看”向的点。这个点决定了相机的前进方向(Forward 向量)
// up:相机的向上方向(Up 向量),通常与相机的前进方向垂直,这个向量用于确定相机的右侧方向(Right 向量)和确保相机的坐标系是正交的
// 相机源点、目标、向上方向
// R、U、F 分别是 Right、Up 和 Forward 向量
mat3 getCameraMat(vec3 ro, vec3 target, vec3 up) {
vec3 f = normalize(target - ro); // 计算 Forward 向量(F)
// 叉积 cross(a, b) 的结果是一个垂直于向量 a 和 b 的向量
// 注意:由于使用的是左手坐标系,所以是使用 up 叉乘 f,而不是反过来进行叉乘,进行叉乘运算时一定要注意其方向性!
vec3 r = cross(up, f); // Right 向量(R)是 Forward 向量和 Up 向量的叉积,表示相机的右侧方向
vec3 u = normalize(cross(f, r)); // 为了确保 Up 向量垂直于 Forward 向量,需要重新计算 Up 向量为 Right 向量和 Forward 向量的叉积
return mat3(r, u, f);
}
// rayOrigin 代表视线(或光线)的起点
// rayDirection 代表视线(或光线)的方向
float rayMarching(vec3 rayOrigin, vec3 rayDirection) {
// disTotal 是视线(或光线)从 rayOrigin 出发后行进的总距离
float disTotal = 0.0;
const float MAX_STEPS = 100.0;
float SURFACE_DIST = 0.0001;
for (float i = 0.0; i < MAX_STEPS; i += 1.0) {
// 视线(或光线)的当前位置 pos
// rayDirection * disTotal 计算视线(或光线)在方向 rayDirection 上行进距离 disTotal 后的向量,然后将这个向量加到源点 rayOrigin 上,得到新的位置 pos。
vec3 pos = rayOrigin + disTotal * rayDirection;
// dS 表示从当前视线(或光线)位置 p 到最近的场景物体表面的距离
float dS = getDist(pos);
disTotal += dS;
// 如果从当前位置到球面的距离 dS 小于某个阈值 SURFACE_DIST,则可能表示视线(或光线)已经“击中”了表面,因此退出循环
// 因为点 p 可能在物体内,float dS = getDist(pos) 中的 dS 会返回负值
// 如果视线(或光线)行进的距离 disTotal 大于某个最大距离 MAX_DIST,则退出循环,可能是因为视线(或光线)已经行进得太远而没有“击中”任何物体
if (dS < SURFACE_DIST || disTotal > MAX_DIST) {
break;
}
}
return disTotal;
}
// 计算一个 3D 点 p 与光源之间的漫反射光照强度
float getLightDif(vec3 lightPos, vec3 p) {
float SHADOW = 0.1;
// 计算从点 p 到光源的方向,方向的箭头指向 lightPos
// 首先计算从点 p 到光源 lightPos 的向量
// 然后,使用 normalize 函数将这个向量标准化(或归一化),使其长度为 1
// 标准化后的向量 l 表示,从点 p 指向光源的方向
vec3 l = normalize(lightPos - p);
vec3 n = getNormal(p);
// 计算漫反射光照强度
// 使用点积(dot 函数)来计算法线向量 n 和光源方向向量 l 之间的角度的余弦值
// 这个余弦值表示了光源方向和表面法线之间的“对齐”程度,从而决定了光照的强度
// 由于余弦值可能是负的(当光源在表面的背面时),使用 max 函数确保结果始终是非负的
// 因此,dif 变量存储了漫反射光照的强度
float dif = max(dot(n, l), 0.0);
// 如果不需要阴影
// return dif;
// 阴影检测(偏移起点防止自相交)
float dis = rayMarching(p + 0.01 * n, l);
if (dis < length(lightPos - p)) {
// 存在遮挡则减弱光照
dif *= SHADOW;
}
return dif;
}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
// 归一化 uv 的坐标范围到 [-1, 1]
// [0, 1] -> [-0.5, 0.5]<* 0.5>
vec2 uv = (fragCoord.xy - u_resolution.xy * 0.5) / min(u_resolution.y, u_resolution.x);
// 这行会导致格子样式错乱
// uv *= 5.0;
vec3 color = vec3(0.0);
float t = u_time * MOVE_SPEED;
vec3 lightPos = vec3(3.0, 8.0, -5.0 - t);
// 相机信息
vec3 cameraPos = vec3(0.0, 3.0, -8.0 - t);
vec3 cameraUp = vec3(0.0, 1.0, 0.0);
vec3 cameraTarget = vec3(0.0, 0.0, 0.0 - t);
// rayDirection 是视线(或光线)的方向向量,表示视线(或光线)沿着哪个方向行进
// 之所以 * vec3(uv, 1.0),uv.x 表示水平方向的偏移,uv.y 表示垂直方向的偏移,1.0 表示沿着相机的前进方向(即深度方向)的偏移
// 1.0 表示沿着相机的前进方向(即深度方向)的偏移
// vec3(uv, 1.0) 将二维的 uv 坐标扩展为一个三维向量,其中 z 分量为 1.0
vec3 rayDirection = getCameraMat(cameraPos, cameraTarget, cameraUp) * vec3(uv, 1.0);
// 视线(或光线)步进行进了多少距离
float rayDist = rayMarching(cameraPos, rayDirection);
// 视线(或光线)的当前位置 p(从相机发出一条射线,一直延伸到接触了物体表面,这之间的距离)
// rayDirection * rayDist 计算视线(或光线在方向 rayDirection 上行进距离 rayDist 后的向量,然后将这个向量加到源点 cameraTarget 上,得到新的位置 p。
vec3 pointOfCameraTouchObject = cameraPos + rayDirection * rayDist;
// 点 p 与光源之间的漫反射光照强度
float lightDif = getLightDif(lightPos, pointOfCameraTouchObject);
// 天空颜色
vec3 skyColor = getSkyColor(rayDirection);
// 除天空外的颜色
vec3 exceptSkyColor = vec3(0.0);
if (pointOfCameraTouchObject.y < IS_LESS_THAN_GROUND_Y) { // 如果在地面上,返回地面的黑白格子光照
exceptSkyColor = groundGrid(pointOfCameraTouchObject) * lightDif;
} else { // 如果不在地面上,返回普通的漫反射光照
exceptSkyColor = vec3(lightDif);
}
if (rayDist < MAX_DIST) { // 击中物体,直接使用物体颜色
color = exceptSkyColor;
} else { // 未击中物体,混合天空颜色
float mixFactor = pow(smoothstep(0.0, -0.02, rayDirection.y), 0.1);
color = mix(skyColor, exceptSkyColor, mixFactor);
}
gl_FragColor = vec4(color, 1.0);
}
`)
})
}
</script>