本文正在参加「金石计划」 之前的3d飞机游戏,里面就有爆炸效果。 这次,就来重复造轮子,详解一下这个爆炸。
爆炸效果,分两个部分, 一部分是形变,另一部分就是颜色的变化。我们这个爆炸很简单,不是蘑菇云,就是一个简单的不均匀的四面八方的膨胀。 因为,是空中炸的,不像在地上会受到地面的限制。
准备工作
准备一个基础的场景,就直接拿之前的地月场景接着用吧 。
一个球体,用来炸的。
这里因为要自己写着色器,为了避免低级错误, 还是用之前的方法,着色器单独写两个文件,有良好的语法检查。 而,又因为有了语法检查,所以这里要写完整的着色器。 所以,我使用了RawShaderMaterial。
const vertexShader = await ((await fetch('./explosion.vert')).text());
const fragmentShader = await ((await fetch('./explosion.frag')).text());
const mat = new RawShaderMaterial({
vertexShader, fragmentShader,
uniforms: {
u_Time: { value: -1 },
u_Map1:{value: texture}
},
transparent:true
})
const ballGeo =new SphereGeometry(4);
const boom = new Mesh(ballGeo, mat);
下面是基础的着色器代码,什么都没有,只能渲染出红色的物体。也就多一个时间,后面更新动画用的, 还有一个v_noise,这是要在顶点着色器里计算,插值传给片元着色器,以减少片元着色器的负担。
顶点着色器
//explosion.vert
#version 300 es
// threejs 用
#define attribute in
#define varying out
attribute vec3 position ;
attribute vec3 normal;
attribute vec2 uv;
uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform float u_Time ;
varying float v_noise ;
void main(){
float t = u_Time* .001 ;
float noise ;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}}
片元着色器
#version 300 es
#define varying in
layout(location = 0) out highp vec4 pc_fragColor;
#define gl_FragColor pc_fragColor
uniform float u_Time;
uniform vec2 uv ;
in float v_noise ;
vec3 hsbToRgb( in vec3 c){
vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) -3.0 ) -1.0, 0.0, 1.0 );
rgb =rgb * rgb *(3.0 - 2.0 * rgb) ;
return c.z * mix(vec3(1.0), rgb, c.y);
}
void main() {
float t = u_Time * .001 ;
gl_FragColor = vec4( 1.,0,0,1. );
}
形变
这里的形变非常简单,就是一个球体,然后在这个基础上,让顶点沿着法线,放射出去。这个放射的距离,就用一个噪声函数来决定,简单一点的话,先来试一下,直接用uv映射到噪声。
顶点都向外扩展,且不规则,这就是爆炸的形变。 大概就像下图的第四个那样。
然后在这个形变的基础上,增加这个球的半径,就是放大这个球,修改scale就可以了。
偷个懒,用二维噪声
就用之前写的双线性差值那个经典的噪声,把uv传进去,拿到偏移量。
// 顶点着色器
v_noise =noise2d(uv*100.+ t);
vec3 offset =v_noise* normal;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position + offset, 1.0 );
scale 这里我用的是指数爆炸。
// scale 的范围是 [.05 , 2] 0 4
let scale = 2 ** (t*.04 -2);
boom.scale.set(scale, scale, scale);
单色看起来,看不出啥,换个彩色的看看。
效果确实好多了,但是,可以看到在球的极点处,裂开了,其实在某经线处也是裂开的。 为什么会在这炸裂呢,感兴趣的掘友可以去看一下球,这个几何体的uv 是怎么来的,等距圆柱投影。 因为它的uv在这里是不连续的,所以这里的噪声偏移也就会出问题。
// 片元着色器
//v_noise是顶点着色器传过来的
vec3 color = hsbToRgb(vec3(v_noise* .9 + .1,.5,t)) ;
换上三维的噪声
原本的噪声是一个三维立体噪声, 如果我直接写一个三维立体噪声呢。 就用三线性插值,这样我就能让它连续起来。
//顶点着色器
float noise3d(vec3 p){
vec3 f = fract(p) ;
vec3 i = floor(p);
float x1 = mix(random3d(vec3(i)), random3d(vec3(i.x+1. ,i.y,i.z)), f.x);// 只在整数部分随机 小数部分补间,f已经是小数
float x2 = mix(random3d(vec3(i.x,i.y+1.,i.z)), random3d(vec3(i.x+1. ,i.y+1.,i.z)), f.x);// 只在整数部分随机 小数部分补间,f已经是小数
float x3 = mix(random3d(vec3(i.x,i.y,i.z+1.)), random3d(vec3(i.x+1. ,i.y,i.z +1.)), f.x);// 只在整数部分随机 小数部分补间,f已经是小数
float x4 = mix(random3d(vec3(i.x,i.y+1.,i.z+1.)), random3d(vec3(i.x+1. ,i.y+1.,i.z+1.)), f.x);// 只在整数部分随机 小数部分补间,f已经是小数
float y1 = mix(x1,x2,f.y) ; // 这就是双线性插值 ,二维有四个点,先在x方向上插值,得到两个值,这两个值再在y上插值
float y2 = mix(x1,x2,f.y) ;
float d = mix( y1,y2 , f.z) ;// 三线性插值
return d ;
}
.....
offset = noise3d(position+ t) * normal;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position + offset, 1.0 );
可以看到,确实连续起来了。 看来,三维的就得用三维啊,不能偷懒。
颜色
配色是个大问题, 一开始我的思路是红黄白,最终是白。 因为光芒越来越强,强到最后就只剩白光了。 观察一下。
看起来似乎大有可为,只要保证红色通道满上, 绿色通道随机一部分,蓝色随机一小部分。然后再根据时间补上缺失的部分,保证最终的颜色是白色即可。
// 片元着色器
vec3 red = vec3(1.,0,0), yellow = vec3(1.,1. ,0 );
vec3 boom = vec3(.9 + v_noise*.1, .6+ v_noise*.4 , v_noise* .5 );
float t = u_Time * .001 ;
boom +=t*t*vec3(.1,.4,.9) ;
vec3 color = hsbToRgb(vec3(v_noise* .9 + .1,.5,t)) ;
// vec3 color = texture(u_Map1, vec2(.5,v_noise*1.3 + t)).rgb ;
gl_FragColor = vec4( boom,1. );
这里 ,我处理输入的时间在0-1000之间,所以t的范围就是[0,1]。
看上去好像还不错。 但是,还是差了不少。 看看答案是怎么做的, 这种调色真是为难我了。
爆炸光谱
原本的效果是使用一个纹理,这个纹理长下面这样。 然后就随机uv,在这些谱色中取色。 只要随机y方向即可,x就固定为0.5。
在自定义的材质上加上一个uniform变量,然后在片元着色器中使用它。
const textureLoader = new TextureLoader();
const texture = textureLoader.load('../shader/texture/explosion.png');
mat.uniforms.u_Map1 = texture;
vec3 color = texture(u_Map1, vec2(.5,v_noise*1.3 + t)).rgb ;
gl_FragColor = vec4( color,1. );
}
这配色效果真不错啊。 果然,有些东西用经验就好了。
加上消失
炸完了之后,应该就什么都没了。 这里还是用透明度来实现。threejs这里,就算使用了RawShaderMaterial,也还是要开启transparent才行。 可以直接用opacity属性来实现,但是既然是造轮子,这里就在着色器里实现。
目标效果是炸完之后, 渐变消失,也就是不透明度,最后要迅速归零。 这里就直接用立方来实现。前面说了t的范围是[0,1]. t的立方会缓慢增长,直到接近1的时候会加速。
gl_FragColor = vec4( color,1.- t*t*t );
简单封装一下
前面的代码,其实完全独立,不需要改造什么。 这里也就是再加上一些东西。 上面爆炸物的位置,我是直接用了月球的位置。 也就是说,需要给爆炸物一个爆炸的目标。 然后,还需要提供一个开始和结束的方法,便于引爆和收尾。
开始的方法,这里没有,因为有了更新方法了,结束方法,一般是用作移除物体。
mesh的clone方法会复用材质,而shaderMaterial的克隆方法,似乎有点问题,暂时不想研究,就这样吧。
class Explosion{
constructor(parent = new Object3D(), duration =1000){
// shdader 材质的克隆方法有点问题。 要不给它提个pr?
this.boom = new Mesh(spheregeo, new RawShaderMaterial({
vertexShader, fragmentShader,
uniforms: {
u_Time: { value: -1 },
u_Map1:{value: texture}
},
transparent:true
})) ;
// this.boom.material = mat.clone();
this.active = true ;
this.duration =duration ;
this.speed = 1000/ duration ;
parent.add(this.boom)
}
update( t = 0){
if( !this.active)return ;
if( t>1000) {
this.onComplete();
}
t*= this.speed ;
// scale 的范围是 [.05 , 2] 0 4
let scale = 2 ** (t*.004 -2);
this.boom.scale.set(scale, scale, scale);
this.boom.material.uniforms.u_Time.value = t ;
}
onComplete(){
// dosomething
}
}
结束
这样一个爆炸效果就完成了。 一个好的效果,其原理是可以很简单的,但是要想效果好,是需要另外一些东西的。
这个爆炸效果,明显是不如原版的。但是,我清楚的知道它的每一步。