Fresnel 边缘光
在三维网格上实现的 Schlick 近似 Fresnel 边缘光,配合 Lambert 漫反射与 Blinn-Phong 高光,用来验证 mesh-3d 预览模式、世界空间法线计算与相机位置 uniform。
静态预览:实时渲染需要启用 JavaScript 与支持 WebGL 2 的浏览器。初始化成功后会自动切换为实时画面。
实时预览未能启动
已自动切换到上方的静态回退预览。下面是浏览器驱动返回的原始编译日志,可直接用于定位 GLSL 错误。
漫反射与高光的底色,以 sRGB 输入并转换到线性空间。
uniform uBaseColor
Fresnel 项的颜色,通常使用偏亮、偏冷的色调。
uniform uRimColor
方向光的颜色,会同时乘到漫反射与高光上。
uniform uLightColor
从表面指向光源的方向向量(世界空间),着色器内部会归一化。
uniform uLightDirection
Fresnel 项的指数。数值越大,边缘光越窄越锐利。
uniform uRimPower
Fresnel 项的整体亮度系数。
uniform uRimIntensity
通过 exp2(mix(3, 11, s)) 映射成 Blinn-Phong 的指数,同时作为高光强度系数。
uniform uSmoothness
让主光绕世界 Y 轴缓慢旋转,方便观察边缘高光的移动。
uniform uRotateLight
运行环境与资源诊断
尚未初始化。
本页目录
效果说明
在一个环面结(torus knot)上实现最常见的实时边缘光:越接近轮廓的地方越亮。它是从全屏效果进入真正三维渲染的第一步,因此这个示例还承担了另外几项验证任务——世界空间法线的正确变换、cameraPosition 这个由 three.js 自动提供的 uniform、透视相机与宽高比变化,以及 mesh-3d 模式下的自动缓慢旋转(方便确认动画确实在推进)。
数学解析
Schlick 近似
严格求解 Fresnel 方程需要折射率 ,实时渲染里几乎都使用 Schlick 近似:
其中 , 是垂直入射时的反射率, 通常取 5(对应真实电介质)。本示例把 直接吸收进 uRimIntensity,并把指数暴露成 uRimPower,于是
越大, 衰减越快,亮边就越窄越贴近轮廓。
漫反射与高光
高光指数使用与 Unity 一致的感知光滑度映射:
也就是 smoothness 从 0 到 1 时,指数从 8 变化到 2048。
实现要点
世界空间还是视图空间
片元着色器里 与 必须处于同一空间。本示例统一使用世界空间:
- 顶点着色器输出
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);
}