跳到主要内容
← 全部 Shader
浏览器实时运行 光照 原始语言:GLSL ES 3.00 浏览器:GLSL ES 3.00

Fresnel 边缘光

在三维网格上实现的 Schlick 近似 Fresnel 边缘光,配合 Lambert 漫反射与 Blinn-Phong 高光,用来验证 mesh-3d 预览模式、世界空间法线计算与相机位置 uniform。

GLSL ES 3.00 三维网格 · torus-knot 等待初始化
Fresnel 边缘光的静态截图:深蓝色的环面结,轮廓处有一圈青色高光

静态预览:实时渲染需要启用 JavaScript 与支持 WebGL 2 的浏览器。初始化成功后会自动切换为实时画面。

— 0.0 s
预设
#1F4D7A

漫反射与高光的底色,以 sRGB 输入并转换到线性空间。 uniform uBaseColor

#57E6FF

Fresnel 项的颜色,通常使用偏亮、偏冷的色调。 uniform uRimColor

#FFF2DC

方向光的颜色,会同时乘到漫反射与高光上。 uniform uLightColor

(0.40, 0.85, 0.35)

从表面指向光源的方向向量(世界空间),着色器内部会归一化。 uniform uLightDirection

3.00

Fresnel 项的指数。数值越大,边缘光越窄越锐利。 uniform uRimPower

1.20

Fresnel 项的整体亮度系数。 uniform uRimIntensity

0.60

通过 exp2(mix(3, 11, s)) 映射成 Blinn-Phong 的指数,同时作为高光强度系数。 uniform uSmoothness

开

让主光绕世界 Y 轴缓慢旋转,方便观察边缘高光的移动。 uniform uRotateLight

运行环境与资源诊断
尚未初始化。
本页目录

效果说明

在一个环面结(torus knot)上实现最常见的实时边缘光:越接近轮廓的地方越亮。它是从全屏效果进入真正三维渲染的第一步,因此这个示例还承担了另外几项验证任务——世界空间法线的正确变换、cameraPosition 这个由 three.js 自动提供的 uniform、透视相机与宽高比变化,以及 mesh-3d 模式下的自动缓慢旋转(方便确认动画确实在推进)。

数学解析

Schlick 近似

严格求解 Fresnel 方程需要折射率 n1,n2n_1, n_2,实时渲染里几乎都使用 Schlick 近似:

F(θ)≈F0+(1−F0) (1−cos⁡θ)pF(\theta) \approx F_0 + (1 - F_0)\,(1 - \cos\theta)^{p}

其中 cos⁡θ=max⁡(N⋅V, 0)\cos\theta = \max(\mathbf{N}\cdot\mathbf{V},\ 0),F0F_0 是垂直入射时的反射率,pp 通常取 5(对应真实电介质)。本示例把 F0F_0 直接吸收进 uRimIntensity,并把指数暴露成 uRimPower,于是

rim=crim⋅(1−cos⁡θ)p⋅I\text{rim} = \mathbf{c}_{\text{rim}} \cdot (1-\cos\theta)^{p} \cdot I

pp 越大,(1−cos⁡θ)p(1-\cos\theta)^p 衰减越快,亮边就越窄越贴近轮廓。

漫反射与高光

diffuse=cbase⊙clight⋅max⁡(N⋅L,0)\text{diffuse} = \mathbf{c}_{\text{base}} \odot \mathbf{c}_{\text{light}} \cdot \max(\mathbf{N}\cdot\mathbf{L}, 0) specular=clight⋅max⁡(N⋅H,0)s,H=normalize⁡(L+V)\text{specular} = \mathbf{c}_{\text{light}} \cdot \max(\mathbf{N}\cdot\mathbf{H}, 0)^{s},\qquad \mathbf{H} = \operatorname{normalize}(\mathbf{L} + \mathbf{V})

高光指数使用与 Unity 一致的感知光滑度映射:

s=2 lerp⁡(3, 11, smoothness)s = 2^{\,\operatorname{lerp}(3,\ 11,\ \text{smoothness})}

也就是 smoothness 从 0 到 1 时,指数从 8 变化到 2048。

实现要点

世界空间还是视图空间

片元着色器里 N\mathbf{N} 与 V\mathbf{V} 必须处于同一空间。本示例统一使用世界空间:

  • 顶点着色器输出 vPositionWS = (modelMatrix * vec4(position, 1.0)).xyz;
  • 法线用 normalize(mat3(modelMatrix) * normal)。

这里没有使用 three.js 提供的 normalMatrix,因为它是 modelViewMatrix 的逆转置,得到的是视图空间法线。如果直接把它和世界空间的 cameraPosition 混用,边缘光的位置会随相机移动而错误漂移。mat3(modelMatrix) 只在非均匀缩放时才与逆转置不等价,本页的网格缩放恒为 1,所以两者一致。

cameraPosition 从哪里来

three.js 会在 WebGLRenderer.setProgram() 中把 camera.position 写入名为 cameraPosition 的 uniform(源码位置:WebGLRenderer.js 中的 p_uniforms.map.cameraPosition)。RawShaderMaterial 不会自动声明它,因此着色器里必须自己写 uniform vec3 cameraPosition;,否则链接时它是一个未被赋值的量。同理,modelMatrix、modelViewMatrix、projectionMatrix、viewMatrix、normalMatrix 都由 three.js 上传,但都需要手动声明。

颜色空间

three.js 不会给 RawShaderMaterial 追加任何输出颜色转换代码(WebGLProgram.js 中 parameters.isRawShaderMaterial 为真时前缀只剩自定义 define),所以片元着色器最后必须自己做 linear → sRGB:

vec3 linearToSRGB(vec3 c) {
  vec3 lo = c * 12.92;
  vec3 hi = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;
  return mix(lo, hi, step(vec3(0.0031308), c));
}

而 uniform 一侧则由 THREE.Color.setStyle(hex, SRGBColorSpace) 完成 sRGB → linear,因此整条链路都在标准的线性工作流里。

与其他示例的关系

urp-rimlit-port 使用完全相同的 Fresnel 与高光表达式,但光源数据来自 Unity URP 的 GetMainLight() 映射;两者的着色器函数名(rimLitShininess / rimLitFresnel)刻意保持一致,方便逐行对照。

源码

浏览器 GLSL 与原始 HLSL 分开存放、分开展示。HLSL 仅作参考,浏览器无法直接执行 Unity URP 的 HLSL。

顶点着色器 fresnel-rim.vert glsl GLSL ES 3.00 · 浏览器中实际编译 32 行

three.js 会自动在最前面加入 `#version 300 es`,因此源文件本身不包含该指令。

// Vertex shader for the 3D Fresnel rim-light demo (GLSL ES 3.00 / WebGL 2).
//
// Everything is evaluated in WORLD space so it matches the way URP shaders are
// usually written (TransformObjectToWorld / TransformObjectToWorldNormal).
//
// NOTE: `#version 300 es` is injected by three.js (RawShaderMaterial + GLSL3).

precision highp float;

uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

in vec3 position;   // object space
in vec3 normal;     // object space
in vec2 uv;

out vec3 vPositionWS;
out vec3 vNormalWS;
out vec2 vUv;

void main() {
  vec4 worldPosition = modelMatrix * vec4(position, 1.0);
  vPositionWS = worldPosition.xyz;

  // Equivalent to URP's TransformObjectToWorldNormal for uniform scale.
  // (three.js' `normalMatrix` would give a VIEW-space normal instead.)
  vNormalWS = normalize(mat3(modelMatrix) * normal);

  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
片元着色器 fresnel-rim.frag glsl GLSL ES 3.00 · 浏览器中实际编译 63 行
// Fresnel rim lighting on a 3D mesh: one directional key light, Lambert
// diffuse, Blinn-Phong specular and a Schlick-approximated rim term.
//
// Colour maths happens in linear space; the final sRGB encode is done here
// because three.js adds no output conversion for RawShaderMaterial.
//
// NOTE: `#version 300 es` is injected by three.js (RawShaderMaterial + GLSL3).

precision highp float;

in vec3 vPositionWS;
in vec3 vNormalWS;
in vec2 vUv;

uniform vec3  cameraPosition;   // supplied by three.js, world space
uniform vec3  uBaseColor;       // linear
uniform vec3  uRimColor;        // linear
uniform vec3  uLightColor;      // linear
uniform vec3  uLightDirection;  // direction pointing FROM the surface TOWARD the light
uniform float uTime;            // seconds since the preview started (pausable)
uniform float uRimPower;        // exponent of the Fresnel term
uniform float uRimIntensity;
uniform float uSmoothness;      // 0..1, drives the specular exponent
uniform bool  uRotateLight;

out vec4 fragColor;

vec3 linearToSRGB(vec3 c) {
  vec3 lo = c * 12.92;
  vec3 hi = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;
  return mix(lo, hi, step(vec3(0.0031308), c));
}

void main() {
  vec3 N = normalize(vNormalWS);
  vec3 V = normalize(cameraPosition - vPositionWS);
  vec3 L = normalize(uLightDirection);

  // Orbit the key light around the Y axis so the rim highlight visibly sweeps.
  if (uRotateLight) {
    float a = uTime * 0.6;
    float c = cos(a);
    float s = sin(a);
    L = normalize(vec3(c * L.x + s * L.z, L.y, -s * L.x + c * L.z));
  }

  float ndotl = max(dot(N, L), 0.0);
  vec3 diffuse = uBaseColor * uLightColor * ndotl;

  vec3 H = normalize(L + V);
  float shininess = exp2(mix(3.0, 11.0, clamp(uSmoothness, 0.0, 1.0)));
  float specular = pow(max(dot(N, H), 0.0), shininess) * clamp(uSmoothness, 0.0, 1.0);

  // Schlick approximation: F = F0 + (1 - F0) * (1 - cos(theta))^p
  float cosTheta = clamp(dot(N, V), 0.0, 1.0);
  float fresnel = pow(1.0 - cosTheta, max(uRimPower, 0.0001));

  vec3 ambient = uBaseColor * 0.08;
  vec3 rim = uRimColor * fresnel * uRimIntensity;

  vec3 color = ambient + diffuse + specular * uLightColor + rim;
  fragColor = vec4(linearToSRGB(clamp(color, 0.0, 1.0)), 1.0);
}