这爆炸也太假了

2,532 阅读6分钟

本文正在参加「金石计划」 之前的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映射到噪声。

顶点都向外扩展,且不规则,这就是爆炸的形变。 大概就像下图的第四个那样。

boom.jpg

然后在这个形变的基础上,增加这个球的半径,就是放大这个球,修改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); 

单色看起来,看不出啥,换个彩色的看看。 CPT2304101645-999x609.gif

效果确实好多了,但是,可以看到在球的极点处,裂开了,其实在某经线处也是裂开的。 为什么会在这炸裂呢,感兴趣的掘友可以去看一下球,这个几何体的uv 是怎么来的,等距圆柱投影。 因为它的uv在这里是不连续的,所以这里的噪声偏移也就会出问题。

CPT2304101709-1098x680 (1).gif

//  片元着色器
//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 );


可以看到,确实连续起来了。 看来,三维的就得用三维啊,不能偷懒。

CPT2304101819-1097x799.gif

颜色

配色是个大问题, 一开始我的思路是红黄白,最终是白。 因为光芒越来越强,强到最后就只剩白光了。 观察一下。

image.png 看起来似乎大有可为,只要保证红色通道满上, 绿色通道随机一部分,蓝色随机一小部分。然后再根据时间补上缺失的部分,保证最终的颜色是白色即可。

// 片元着色器
  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]。

看上去好像还不错。 但是,还是差了不少。 看看答案是怎么做的, 这种调色真是为难我了。 CPT2304101844-1115x805.gif

爆炸光谱

原本的效果是使用一个纹理,这个纹理长下面这样。 然后就随机uv,在这些谱色中取色。 只要随机y方向即可,x就固定为0.5。

explosion.png

在自定义的材质上加上一个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. );
}

这配色效果真不错啊。 果然,有些东西用经验就好了。

CPT2304101917-1110x805.gif

加上消失

炸完了之后,应该就什么都没了。 这里还是用透明度来实现。threejs这里,就算使用了RawShaderMaterial,也还是要开启transparent才行。 可以直接用opacity属性来实现,但是既然是造轮子,这里就在着色器里实现。

目标效果是炸完之后, 渐变消失,也就是不透明度,最后要迅速归零。 这里就直接用立方来实现。前面说了t的范围是[0,1]. t的立方会缓慢增长,直到接近1的时候会加速。

 gl_FragColor = vec4( color,1.- t*t*t );

简单封装一下

前面的代码,其实完全独立,不需要改造什么。 这里也就是再加上一些东西。 上面爆炸物的位置,我是直接用了月球的位置。 也就是说,需要给爆炸物一个爆炸的目标。 然后,还需要提供一个开始和结束的方法,便于引爆和收尾。

开始的方法,这里没有,因为有了更新方法了,结束方法,一般是用作移除物体。

meshclone方法会复用材质,而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
    }
}

结束

这样一个爆炸效果就完成了。 一个好的效果,其原理是可以很简单的,但是要想效果好,是需要另外一些东西的。

这个爆炸效果,明显是不如原版的。但是,我清楚的知道它的每一步。