Skip to content

SinCos

点击运行

<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="sinCos" 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('sinCos')
    const glslCanvas: any = new module.default(canvas)
 
    glslCanvas.load(`
      precision highp float;

      uniform float u_time;
      uniform vec2 u_resolution;

      const int AA = 8; // 抗锯齿级别,可以根据需要调整

      void main() {
        vec2 fragCoord = gl_FragCoord.xy;
        vec2 uv = (fragCoord - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);
        uv *= 1.0;

        float amplitude = 1.0;
        float frequency = 1.0;

        // 没有 AA 采样
        // float y = amplitude * sin(u_time) * cos(uv.x * frequency + u_time);
        // if(uv.y < y) {
        //   color = vec3(1.0);
        // }

        // 累积采样结果
        float totalSampled = 0.0;

        // 对每个像素进行多次采样
        for (int i = 0; i < AA; ++i) {
          for (int j = 0; j < AA; ++j) {
            // 计算当前采样点的偏移量,使用随机偏移量或更均匀的分布方式
            vec2 offset = (vec2(float(i), float(j)) + vec2(0.5)) / float(AA) - 0.5;

            // 根据偏移量和原始 UV 坐标计算采样点的 UV 坐标。这里再次考虑了分辨率的纵横比,以确保采样均匀
            vec2 sampleUV = uv + offset / min(u_resolution.y, u_resolution.x);

            // 根据采样点的 UV 坐标和时间计算波形的高度
            float y = amplitude * sin(u_time * 0.4) * cos(sampleUV.x * frequency + u_time * 0.4);
 
            // 使用 step 函数判断采样点的 y 坐标是否小于波形的 y 坐标。如果是,则返回 1;否则返回 0
            // float sample = step(sampleUV.y, y); // step 函数返回 0 或 1
            float sample = smoothstep(sampleUV.y - 0.01, sampleUV.y + 0.01, y);

            // 将判断结果累加到 totalSampled 中
            totalSampled += sample;
          }
        }
        
        // 计算平均采样结果,并根据结果决定颜色
        float alpha = totalSampled / float(AA * AA); // 平均后的透明度值
        vec3 color = vec3(1.0) * alpha; // 根据透明度值决定颜色

        gl_FragColor = vec4(color, 1.0);
      }`)
  })
}

</script>

星星

点击运行

<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="stars" 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('stars')
    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;

      // 旋转
      mat2 rot(float a) {
        float s = sin(a);
        float c = cos(a);
        return mat2(c, -s, s, c);
      }

      // uv , 星辉的亮度
      float star(vec2 uv, float flare) {
        // 画圆形
        float d = length(uv);
        float m = 0.05 / d;

        // 大的十字
        float r = max(0.0, 1.0 - abs(uv.x * uv.y * 1000.0));
        m += r * flare;

        // 小的十字
        uv *= rot(3.1415926 / 4.0);
        float r2 = max(0.0, 1.0 - abs(uv.x * uv.y * 1000.0));
        m += r2 * flare * 0.25;

        // d > 0.8 为 0,d < 0.2 为 1
        return  m * smoothstep(0.8, 0.2, d);
      }

      // 将星星绘制的更加错落有致,利用一个 Hash 函数来让每个格子里的星星发生一点点偏移
      float hash21(vec2 p) {
        p = fract(p * vec2(123.45, 456.21));
        p += dot(p, p + 45.32);
        return fract(p.x + p.y);
      }

      vec3 layer(vec2 uv) {
        vec3 col = vec3(0.0);

        float splitNumb = 3.0;

        vec2 st = fract(uv * splitNumb) - 0.5;

        // 由于想要的是每个格子产生的随机数需要保持一致,所以不能够使用 st 坐标来产生随机数
        // 而是要获取每个格子的 index 或者说是 id 来产生随机数
        // 即每次遍历都是该 id 为参数的随机函数产生的结果
        // 使用 floor 函数来获取每个格子的 id。
        vec2 id = floor(uv * splitNumb);

        // 此时会有断层出现
        // float offset = hash21(id) * 0.3;
        // col += star(st - offset, 1.0);

        // 解决断层
        // y -> -1.0,0.0,1.0
        // x -> -1.0,0.0,1.0
        // 在一个 3x3 的邻域内,对于每个邻域点,都计算一个基于哈希函数的内部偏移
        // 并使用这个偏移和邻域偏移来调用一个 Star 函数来计算某种效果(可能是星星或亮点)
        // 然后将这个效果累加到 col 上。这种技术常用于生成复杂的纹理、噪声或其他视觉效果
        // ------------------------------------------------------------------------
        // 这两个循环,正在遍历一个 3x3 的网格(以当前点为中心),这通常用于某种邻域操作或采样
        // 代码遍历了当前像素 uv 周围的一些格子(即邻近的格子),并对每个格子应用星星效果
        // 循环遍历了 y 值从 -1 到 1 的整数范围,代表垂直方向上的三个点(通常是中心点以及上下两个相邻的点)
        for (float y = -1.0; y <= 1.0; y++) {
          // 循环遍历了 x 值从 -1 到 1 的整数范围,代表水平方向上的三个点(通常是中心点以及左右两个相邻的点)
          for (float x = -1.0; x <= 1.0; x++) {

            // 创建了一个二维向量 neighborOffset,它表示从当前点到邻域中某个点的偏移
            // 用于定义当前正在处理的邻居(或称为“邻近格子”)的偏移量
            // 代码的目的是遍历当前像素所在位置(由 uv 表示)附近的格子或“细胞”(cells),并为每个这样的格子或细胞计算星星的效果
            vec2 neighborOffset = vec2(x, y);

            // 使用哈希函数 Hash21,结合一个标识符 id 和邻域偏移来计算一个浮点数值 n
            // 这是一个伪随机值,用于生成某种噪声或纹理
            float n = hash21(id + neighborOffset);

            // 计算了一个内部偏移 innerOffset,它基于之前计算的哈希值 n
            // 可以把下面三个 innerOffset 分别查看效果
            // fract 函数返回其参数的小数部分,所以 fract(n * 52.) 将 n 乘以 52 并取其小数部分
            // 这会为 innerOffset 引入某种随机或周期性的变化
            // vec2 innerOffset = vec2(n - 0.5, n - 0.3);
            // vec2 innerOffset = vec2(x - 0.5, y - 0.3);
            vec2 innerOffset = vec2(n - 0.5, fract(n * 25.) - 0.5);
            
            // 通过 size 来改变星辉的亮度
            float size = fract(n * 345.34) * 0.8 + 0.2;
            float flare = smoothstep(0.4, 0.8, size);

            // 星星颜色
            vec3 color = sin(vec3(0.2, 0.5, 0.9) * fract(n * 2345.2) * 6.28) * 0.5 + 0.5;
            color *= vec3(0.3 + size * 0.2, 0.5, 0.5 + size * 1.5);

            // st 是一个二维向量,表示当前像素或片段的屏幕空间位置(通常是一个 (x, y) 坐标对)
            // neighborOffset 是一个二维向量,表示从当前位置到某个邻接位置的偏移量(例如,上下左右或对角线方向的偏移)
            // innerOffset 同样是一个二维向量,是基于某种哈希函数或其他随机机制生成的,用于为邻接位置引入额外的随机或周期性偏移
            // st - neighborOffset - innerOffset 的结果就是计算出一个新的二维位置,相对于当前位置的一个偏移点,并且这个偏移点被 innerOffset 进一步随机化或扰动了
            // 把 innerOffset 换成 0.1 之类的值,则是所有的星星都偏移固定的值
            float s = star(st - neighborOffset - innerOffset, flare);
            col += s * size * color;
          }
        }

        vec2 nuv = fract(uv * splitNumb);

        // 网格
        // if (abs(nuv.x) < fwidth(nuv.x) || abs(nuv.y) < fwidth(nuv.y)) {
        //   col = vec3(1.0);
        // }
        // if (abs(uv.x) < fwidth(uv.x)) {
        //   col = vec3(0.0, 1.0, 0.0);
        // }
        // if (abs(uv.y) < fwidth(uv.y)) {
        //   col = vec3(1.0, 0.0, 0.0);
        // }

        return col;
      }

      void main() {
        vec2 fragCoord = gl_FragCoord.xy;
        vec2 uv = (fragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);
        // vec3 col = layer(uv);

        vec3 col = vec3(0.0);
        float t = u_time * 0.1;
        // 把 uv 旋转
        uv *= rot(t);

        for (float i = 0.0; i < 1.0; i += 1. / 2.0) {
          // t 的作用是模拟深度,z 轴的方向
          float depth = fract(i + t);
          float fade = smoothstep(0.0, 0.3, depth) * smoothstep(1.0, 0.9, depth);
          float scale = mix(20.0, .05, depth);
          col += layer(uv * scale + i * 435.32) * fade;
        }

        gl_FragColor = vec4(col, 1.0);
      }`)
  })
}

</script>

合并两个物体、光照、阴影

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="lightShadow" 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('lightShadow')
    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;


      // minVec2 比较两个 vec2 类型的向量 a 和 b,返回其中第一个分量(x)较小的那个向量
      // 假设一个函数
      // vec2 tempFun(vec3 p) {
      //   vec2 a = vec2(sdSphere(p - vec3(0.0, 1.0, 0.0), 1.0, 2.0);
      //   vec2 b = vec2(sdPlane(p), 1.0);
      //   return minVec2(a, b);
      // }
      // 光线的当前位置 p 为 (0.5, 0.5, 0.5)
      // 球体的距离:sdSphere(p - vec3(0.0, 1.0, 0.0), 1.0) = length(vec3(0.5, -0.5, 0.5)) - 1.0 ≈ 0.3536
      // 平面的距离:sdPlane(p) = 0.5
      // 比较两个距离:min(vec2(0.3536, 2.0), vec2(0.5, 1.0)),返回 vec2(0.3536, 2.0),表示最近的物体是球体
      // 通过这种方式,tempFun 函数可以有效地组合多个物体,并在光线行进算法中正确地处理它们
      vec2 minVec2(vec2 a, vec2 b) {
        return a.x < b.x ? a : b;
      }

      // 计算一个点 p 到一个无限大水平平面(地面)的距离
      // 这个平面通常被定义为 y=0 的平面,即地面位于 y 轴的零点
      float getGroundDist(vec3 p) {
        float groundDist = p.y;
        return groundDist;
      }

      // 计算一个点 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;
      }

      // 计算一个点 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;
      }

      // 多个物体的合并的 dist 取 min,即物体距离相机最近的 dist
      float getDist(vec3 pos) {
        vec3 SPHERE_POS = vec3(0.5, 1.0, 6.0);
        float SPHERE_RADIUS = 1.0;

        vec3 CUBE_POS = vec3(0.0, 0.0, 6.0);
        vec3 CUBE_SIZE = vec3(1.0, 1.0, 1.0);
        
        float groundDist = getGroundDist(pos);
        float cubeDist = getCubeDist(pos, CUBE_POS, CUBE_SIZE);
        float sphereDist = getSphereDist(pos, SPHERE_POS, SPHERE_RADIUS);
        return min(groundDist, min(cubeDist, sphereDist));
      }

      // 联合形状
      // 两个形状距离值的最小值,表示两个形状的并集
      float unionShape(float shapeA, float shapeB) {
        return min(shapeA, shapeB);
      }


      // 左手坐标系
      // 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) {
        // d0 是光线从 rayOrigin 出发后行进的总距离
        float d0 = 0.0;

        const float MAX_STEPS = 256.0;
        float SURFACE_DIST = 0.001;
        float MAX_DIST = 100.0;
        
        for (float i = 0.0; i < MAX_STEPS; i += 1.0) {
          // 光线的当前位置 pos
          // rayDirection * d0 计算光线在方向 rayDirection 上行进距离 d0 后的向量,然后将这个向量加到源点 rayOrigin 上,得到新的位置 pos。
          vec3 pos = rayOrigin + d0 * rayDirection;
          // dS 表示从当前光线位置 p 到最近的场景物体表面的距离
          float dS = getDist(pos);
          d0 += dS;
          // 如果从当前位置到球面的距离 dS 小于某个阈值 SURFACE_DIST,则可能表示光线已经“击中”了表面,因此退出循环
          // 因为点 p 可能在物体内,float dS = getDist(pos) 中的 dS 会返回负值
          // 如果光线行进的距离 d0 大于某个最大距离 MAX_DIST,则退出循环,可能是因为光线已经行进得太远而没有“击中”任何物体
          if (dS < SURFACE_DIST || d0 > MAX_DIST) {
            break;
          }
        }
        return d0;
      }

      // 黑白网格
      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);

        // mix(c1, c2, s) 根据 s 的值在 c1 和 c2 之间进行插值
        // 当 s 为 0 时,返回 c1(白色)
        // 当 s 为 1 时,返回 c2(黑色)
        return mix(c1, c2, s);
      }

      // 在基于距离场(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);
      }


      // 计算一个 3D 点 p 与光源之间的漫反射光照强度
      vec3 getLight(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;
        }

        // 地面的黑白格子
        // 由于 getDist 中有 planeDist,所以 p 点包含了地面
        // 修改不同的 groundY,会看到最远处的变化
        float groundY = 0.001;
        if(p.y < groundY) {
          return groundGrid(p) * dif;
        }

        return vec3(dif);
      }

      void main() {
        vec2 fragCoord = gl_FragCoord.xy;
        vec2 uv = (fragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);

        vec3 color = vec3(0.0, 0.0, 0.0);


        // SuperSampling(超级采样)。对每个像素进行多次采样并取平均值:
        const int samples = 2;
        for (int i = 0; i < samples; ++i) {
          for (int j = 0; j < samples; ++j) {
            vec2 offset = vec2(float(i), float(j)) / float(samples) - 0.5;
            vec2 uvSample = uv + offset / min(u_resolution.y, u_resolution.x);

            // 计算每个子样本的颜色
            vec3 sampleColor = vec3(0.0);

            vec3 lightPos = vec3(2.0, 5.0, 2.0);
            lightPos.xz += vec2(sin(u_time) + 2.0, cos(u_time)) * 2.0;

            // 相机信息
            vec3 cameraPos = vec3(0.0, 3.0, 0.0);
            vec3 cameraUp = vec3(0.0, 1.0, 0.0);
            vec3 cameraTarget = vec3(0.0, 1.0, 6.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);

            // 视线(或光线)步进行进了多少距离
            float rayDist = rayMarching(cameraPos, rayDirection);

            // 视线(或光线)当前位置 p(从相机发出一条射线,一直延伸到接触了物体表面,这之间的距离)
            // rayDirection * rayDist 计算视线(或光线在方向 rayDirection 上行进距离 rayDist 后的向量,然后将这个向量加到源点 cameraTarget 上,得到新的位置 p。
            vec3 pointOfCameraTouchObject = cameraPos + rayDirection * rayDist;

            vec3 sampleLightColor = getLight(lightPos, pointOfCameraTouchObject);
            sampleColor = sampleLightColor;

            // 累加颜色
            color += sampleColor;
          }
        }
        // 计算平均颜色
        color /= float(samples * samples);


        gl_FragColor = vec4(color, 1.0);
      }`)
  })
}

</script>