Three.js-体积光

别人的原理

参考自这个文章

体积光也是基于后处理,用叠加texture的方式来实现,整个流程如下:

  1. Render the scene, with the light source as white and all occluding objects as black, to a texture.
  2. With a shader, apply a parameterized radial blur to that texture to create the lighting effect
  3. Additively blend that result over a render of the normally lit scene and display on screen

…estimate the probability of occlusion at each pixel by summing samples along a ray to the light source in image space. The proportion of samples that hit the emissive region versus those that strike occluders gives us the desired percentage of occlusion…

我将这个示例的代码下载过来后,升级到threev134版本,经过调试,发现没有光照效果。看起来像是根本没有将这一层叠加上去。

感觉是用了两个EffectComposer的缘故。

1

光源的光线方向是四散的,我只需要给它做个遮罩模型,只放出来光源球体上的一部分,就可以形成手电筒的效果了。

可以在shadertoy搜索Volumetric Light查看别人做的效果:

Browse (1) - Shadertoy BETA

搜索手电筒Flashlight也可以

我真正使用的原理

Three-js-体积光

这是使用的采样来实现的。

这样有个缺陷:因为是采样,并非正确的物理光照,因此是没有折射、反射、阴影效果的。

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
function getVolumetericLightShader() {
return {
uniforms: {
tDiffuse: { value: null },
// TODO:这个不该写死,应该跟随物体位置动态变化(应该位于物体的背后,这样才能往物体前面发出光线),否则光束就会变成从中心点射向六个方向
lightPosition: { value: new THREE.Vector2(0.5, 0.5) },
exposure: { value: 0.18 },
decay: { value: 0.95 },
// 采样密度
density: { value: 0.8 },
weight: { value: 0.4 },
// 采样率,数值越高,效果越好;数值低了有条纹状的阴影
samples: { value: 100 },
},

vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,

fragmentShader: `
// gl_FragCoord是0-1的坐标范围
// vec2 uv = gl_FragCoord.xy/u_resolution;
varying vec2 vUv;
// 2D纹理采样器
uniform sampler2D tDiffuse;
// 纹理中心点
uniform vec2 lightPosition;
// 曝光度
uniform float exposure;
uniform float decay;
uniform float density;
uniform float weight;
uniform int samples;
const int MAX_SAMPLES = 100;
void main() {
// 当前像素在纹理上的坐标
vec2 texCoord = vUv;
// 相对于纹理中心点的坐标,用于确定【采样范围】
vec2 deltaTextCoord = texCoord - lightPosition;
// 计算单次采样的步进坐标值:采样数越大,采样点步进坐标就越小
// 因为光束是朝着一个方向发射的,因此这里不需要考虑负数的情况;
// 每个点的实际采样范围是一条45度的直线上的samples个点!!!
deltaTextCoord *= 1.0 / float(samples) * density;
// 采样纹理,获取像素原始颜色,这里第一个参数tDiffuse为null,是采用默认的纹理单元0:GL_TEXTURE0
vec4 color = texture2D(tDiffuse, texCoord);
// 亮度衰减
float illuminationDecay = 1.0;
// 采样
for(int i=0; i < MAX_SAMPLES; i++) {
if(i == samples){
break;
}
texCoord -= deltaTextCoord;
vec4 sampleValue = texture2D(tDiffuse, texCoord);
// 给采样点颜色算上亮度衰减系数和权重系数
sampleValue *= illuminationDecay * weight;
// 给原始颜色叠加上采样点颜色
color += sampleValue;
// 修改亮度衰减系数
illuminationDecay *= decay;
}
gl_FragColor = color * exposure;
}
`,
};
}

效果

手电筒

Shader - Shadertoy BETA

这个最简单,帧数也高,用的raymarch

Shader - Shadertoy BETA

Shader - Shadertoy BETA

资料

体积光原理和常用算法

https://zhuanlan.zhihu.com/p/21425792

由这个文章触发的灵感:体积光还可以用于辉光效果

基于分层后处理实现的体积光

https://medium.com/@andrew_b_berg/volumetric-light-scattering-in-three-js-6e1850680a41

参考文章1-基于Three的代码讲解:

(WebGL) Volumetric Light Approximation in Three.js - Blog - (BKcore) Thibaut Despoulain

参考文章2-原理公式讲解:

https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-13-volumetric-light-scattering-post-process

基于Three.js的实现:

GitHub - hsimpson/threejs-volumetric-spotlight: Volumetric spotlight with three.js