火球效果 · Fireball · ▶ 在线运行案例
案例合集: 三维可视化功能案例(threehub.cn)
开源仓库github地址: https://github.com/z2586300277/three-cesium-examples
**400个案例代码: ** 网盘链接

你将学到什么
- ShaderMaterial 自定义着色器实现核心视觉效果
- EffectComposer 多 Pass 后期处理管线
- UnrealBloomPass 辉光 Bloom 效果
- OrbitControls 相机轨道交互
- 自定义 ShaderPass 调色/特效
requestAnimationFrame渲染循环与resize自适应
效果说明
本案例演示 火球效果 效果:原场景渲染后经 EffectComposer 叠加 Bloom/模糊等全屏后期;核心用到 ShaderMaterial、EffectComposer、UnrealBloomPass。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。
核心概念
- Scene / Camera / WebGLRenderer 构成最小渲染闭环;大场景可开
logarithmicDepthBuffer缓解 Z-fighting。 - ShaderMaterial 通过
uniforms+ 自定义 GLSL 控制逐像素/逐点效果;透明粒子常配合depthTest: false。 - EffectComposer 以多 Pass 链式渲染:RenderPass → 特效 Pass → 输出屏幕,替代直接
renderer.render。 - OrbitControls 提供轨道旋转/缩放;开启
enableDamping后需在 animate 中controls.update()。
实现步骤
- 搭建灯光与环境(如有)
- requestAnimationFrame 循环 update + render
代码要点
<!DOCTYPE html><html lang="en"><head>
<title>three.js</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://threejs.org/build/three.module.js",
"three/addons/": "https://threejs.org/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GUI } from "three/addons/libs/lil-gui.module.min.js";
const ENTIRE_SCENE = 0,
BLOOM_SCENE = 1;
const bloomLayer = new THREE.Layers();
bloomLayer.set(BLOOM_SCENE);
const materials = {};
const darkMaterial = new THREE.MeshBasicMaterial({ color: "black" });
const vert = `
varying vec3 vNormal;
varying vec3 camPos;
varying vec2 vUv;
void main() {
vNormal = normal;
vUv = uv;
camPos = cameraPosition;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`;
const frag = `
#define NUM_OCTAVES 5
#define M_PI 3.1415926535897932384626433832795
uniform vec4 resolution;
varying vec3 vNormal;
uniform sampler2D perlinnoise;
uniform sampler2D sparknoise;
uniform float time;
uniform vec3 color0;
uniform vec3 color1;
uniform vec3 color2;
uniform vec3 color3;
uniform vec3 color4;
uniform vec3 color5;
varying vec3 camPos;
varying vec2 vUv;
float setOpacity(float r, float g, float b, float tonethreshold) {
float tone = (r + g + b) / 3.0;
float alpha = 1.0;
if(tone<tonethreshold) {
alpha = 0.0;
}
return alpha;
}
vec3 rgbcol(vec3 col) {
return vec3(col.r/255.0,col.g/255.0,col.b/255.0);
}
vec2 rotate(vec2 v, float a) {
float s = sin(a);
float c = cos(a);
mat2 m = mat2(c, -s, s, c);
return m * v;
}
vec2 UnityPolarCoordinates (vec2 UV, vec2 Center, float RadialScale, float LengthScale){
//https://twitter.com/Cyanilux/status/1123950519133908995/photo/1
vec2 delta = UV - Center;
float radius = length(delta) * 2. * RadialScale;
float angle = atan(delta.x, delta.y) * 1.0/6.28 * LengthScale;
return vec2(radius, angle);
}
void main() {
vec2 olduv = gl_FragCoord.xy/resolution.xy ;
vec2 uv = vUv ;
vec2 imguv = uv;
float scale = 1.;
olduv *= 0.5 + time;
olduv.y = olduv.y ;
vec2 p = olduv*scale;
vec4 txt = texture2D(perlinnoise, olduv);
float gradient = dot(normalize( -camPos ), normalize( vNormal ));
float pct = distance(vUv,vec2(0.5));
vec3 rgbcolor0 = rgbcol(color0);
vec3 rgbcolor1 = rgbcol(color1);
vec3 rgbcolor2 = rgbcol(color2);
vec3 rgbcolor5 = rgbcol(color5);
// set solid background
float y = smoothstep(0.16,0.525,pct);
vec3 backcolor = mix(rgbcolor0, rgbcolor5, y);
gl_FragColor = vec4(backcolor,1.);
// set polar coords
vec2 center = vec2(0.5);
vec2 cor = UnityPolarCoordinates(vec2(vUv.x,vUv.y), center, 1., 1.);
// set textures
vec2 newUv = vec2(cor.x + time,cor.x*0.2+cor.y);
vec3 noisetex = texture2D(perlinnoise,mod(newUv,1.)).rgb;
vec3 noisetex2 = texture2D(sparknoise,mod(newUv,1.)).rgb;
// set textures tones
float tone0 = 1. - smoothstep(0.3,0.6,noisetex.r);
float tone1 = smoothstep(0.3,0.6,noisetex2.r);
// set opacity for each tone
float opacity0 = setOpacity(tone0,tone0,tone0,.29);
float opacity1 = setOpacity(tone1,tone1,tone1,.49);
//set final render
if(opacity1>0.0){
gl_FragColor = vec4(rgbcolor2,0.)*vec4(opacity1);
} else if(opacity0>0.0){
gl_FragColor = vec4(rgbcolor1,0.)*vec4(opacity0);
}
}
`;
const vertcylinder = `
varying vec2 vUv;
void main() {
vUv = uv;
vec3 pos = vec3(position.x/1.,position.y,position.z/1.);
if(pos.y >= 1.87){
pos = vec3(position.x*(sin((position.y - 0.6)*1.27)-0.16),position.y,position.z*(sin((position.y - 0.6)*1.27)-0.16));
} else{
pos = vec3(position.x*(sin((position.y/2. - .01)*.11)+0.75),position.y,position.z*(sin((position.y/2. - .01)*.11)+0.75));
}
gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
}
`;
const fragcylinder = `
varying vec2 vUv;
uniform sampler2D perlinnoise;
uniform vec3 color4;
uniform float time;
varying vec3 vNormal;
vec3 rgbcol(vec3 col) {
return vec3(col.r/255.0,col.g/255.0,col.b/255.0);
}
void main() {
vec3 noisetex = texture2D(perlinnoise,mod(1.*vec2(vUv.y-time*2.,vUv.x + time*1.),1.)).rgb;
gl_FragColor = vec4(noisetex.r);
if(gl_FragColor.r >= 0.5){
gl_FragColor = vec4(rgbcol(color4),gl_FragColor.r);
}else{
gl_FragColor = vec4(0.);
}
gl_FragColor *= vec4(sin(vUv.y) - 0.1);
gl_FragColor *= vec4(smoothstep(0.3,0.628,vUv.y));
}
`;
const vertflame = `
varying vec2 vUv;
varying vec3 camPos;
varying vec3 vNormal;
varying vec3 nois;
uniform sampler2D noise;
uniform float time;
void main() {
vUv = uv;
camPos = cameraPosition;
vNormal = normal;
vec3 pos = vec3(position.x/1.,position.y,position.z/1.);
vec3 noisetex = texture2D(noise,mod(1.*vec2(vUv.y-time*2.,vUv.x + time*1.),1.)).rgb;
if(pos.y >= 1.87){
pos = vec3(position.x*(sin((position.y - 0.64)*1.27)-0.12),position.y,position.z*(sin((position.y - 0.64)*1.27)-0.12));
} else{
pos = vec3(position.x*(sin((position.y/2. - .01)*.11)+0.79),position.y,position.z*(sin((position.y/2. - .01)*.11)+0.79));
}
pos.xz *= noisetex.r;
gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
}
`;
const fragflame = `
varying vec2 vUv;
uniform sampler2D perlinnoise;
uniform sampler2D noise;
uniform vec3 color4;
uniform float time;
varying vec3 camPos;
varying vec3 vNormal;
varying vec3 nois;
vec3 rgbcol(vec3 col) {
return vec3(col.r/255.0,col.g/255.0,col.b/255.0);
}
void main() {
// vec3 noisetex = texture2D(perlinnoise,mod(1.*vec2(vUv.y-time*2.,vUv.x + time*1.),1.)).rgb;
// gl_FragColor += vec4(sin((vUv.y - time)*(20. + vUv.y)));
vec3 noisetex = texture2D(noise,mod(1.*vec2(vUv.y-time*2.,vUv.x + time*1.),1.)).rgb;
// nois = noisetex;
gl_FragColor = vec4(noisetex.r);
if(gl_FragColor.r >= 0.44){
gl_FragColor = vec4(rgbcol(color4),gl_FragColor.r);
}
// else if(gl_FragColor.r >= 0.9){
// // gl_FragColor = vec4(rgbcol(color4),gl_FragColor.r)*0.5;
// }
else{
gl_FragColor = vec4(0.);
}
gl_FragColor *= vec4(smoothstep(0.2,0.628,vUv.y));
// gl_FragColor = vec4(vUv.y - 0.3 );
// gl_FragColor = 1. - vec4(dot(normalize(vNormal),normalize(camPos)).r);
}
`;
let options = {
exposure: 2.8,
bloomStrength: 3.5,
bloomRadius: 0.39,
color0: [0, 0, 0],
color1: [81, 14, 5],
color2: [181, 156, 24],
color3: [66, 66, 66],
color4: [79, 79, 79],
color5: [64, 27, 0],
};
// let options = {
// exposure: 2.8,
// bloomStrength: 3.5,
// // bloomStrength: 0,
// bloomRadius: 0.39,
// color0: [15, 0, 10],
// color1: [13, 6, 5],
// color2: [137, 56, 10],
// color3: [166, 166, 166],
// color4: [237, 149, 67],
// color5: [20, 0, 51],
// };
let gui = new GUI();
let bloom = gui.addFolder("Bloom");
bloom
.add(options, "bloomStrength", 0.0, 5.0)
.name("bloomStrength")
.listen();
bloom.add(options, "bloomRadius", 0.1, 2.0).name("bloomRadius").listen();
bloom.open();
let color = gui.addFolder("Colors");
color.addColor(options, "color0").name("ball0");
color.addColor(options, "color1").name("ball1");
color.addColor(options, "color2").name("ball2");
color.addColor(options, "color4").name("steam");
color.addColor(options, "color5").name("trail");
color.open();
gui.close();
let scene,
camera,
renderer,
controls,
material,
material2,
material3,
bloomPass,
bloomComposer,
composer,
finalPass,
finalComposer;
const width = window.innerWidth,
height = window.innerHeight;
function init() {
createScene();
postProc();
mesh();
animatBloom();
}
function createScene() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(
3.4369982203815655,
3.5239085092722098,
2.994862383531814
);
renderer = new THREE.WebGLRenderer();
renderer.antialias = true;
renderer.setClearColor(new THREE.Color("#000"));
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
controls = new OrbitControls(camera, renderer.domElement);
document.body.appendChild(renderer.domElement);
}
function postProc() {
const renderScene = new RenderPass(scene, camera);
bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5,
0.4,
0.85
);
bloomPass.threshold = options.bloomThreshold;
bloomPass.strength = options.bloomStrength;
bloomPass.radius = options.bloomRadius;
bloomComposer = new EffectComposer(renderer);
bloomComposer.addPass(renderScene);
bloomComposer.addPass(bloomPass);
}
function mesh() {
const geometry = new THREE.SphereGeometry(1, 30, 30);
material = new THREE.ShaderMaterial({
uniforms: {
time: {
type: "f",
value: 0.0,
},
perlinnoise: {
type: "t",
value: new THREE.TextureLoader().load(
FILE_HOST + "images/dissolve/dissolveRamp.png"
),
},
sparknoise: {
type: "t",
value: new THREE.TextureLoader().load(
FILE_HOST + "images/dissolve/dissolveTex.png"
),
},
color5: {
value: new THREE.Vector3(...options.color5),
},
color4: {
value: new THREE.Vector3(...options.color4),
},
color3: {
value: new THREE.Vector3(...options.color3),
},
color2: {
value: new THREE.Vector3(...options.color2),
},
color1: {
value: new THREE.Vector3(...options.color1),
},
color0: {
value: new THREE.Vector3(...options.color0),
},
resolution: { value: new THREE.Vector2(width, height) },
},
vertexShader: vert,
fragmentShader: frag,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.scale.set(0.78, 0.78, 0.78);
mesh.position.set(1 + 0, 0, 0);
scene.add(mesh);
}
function updateDraw(deltaTime) {
material.uniforms.time.value = -deltaTime / (1000 * 2);
material.uniforms.color5.value = new THREE.Vector3(...options.color5);
material.uniforms.color3.value = new THREE.Vector3(...options.color3);
material.uniforms.color2.value = new THREE.Vector3(...options.color2);
material.uniforms.color1.value = new THREE.Vector3(...options.color1);
material.uniforms.color0.value = new THREE.Vector3(...options.color0);
}
function animatBloom(deltaTime) {
requestAnimationFrame(animatBloom);
updateDraw(deltaTime);
controls.update();
bloomPass.strength = options.bloomStrength;
bloomPass.radius = options.bloomRadius;
bloomComposer.render();
}
function handleResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener("load", init);
window.addEventListener("resize", handleResize, false);
</script>
</body>
</html>
完整源码:GitHub
小结
- 本文提供 火球效果 完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
- 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库