👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
文章目录
- 一、🍀前言
-
- [1.1 ☘️GLSL着色器](#1.1 ☘️GLSL着色器)
-
- [1.1.1 ☘️着色器类型](#1.1.1 ☘️着色器类型)
- [1.1.2 ☘️工作原理](#1.1.2 ☘️工作原理)
- [1.1.3 ☘️核心特点](#1.1.3 ☘️核心特点)
- [1.1.4 ☘️应用场景](#1.1.4 ☘️应用场景)
- [1.1.5 ☘️实战示例](#1.1.5 ☘️实战示例)
- [二、🍀使用自定义GLSL 着色器,生成漂流的3D 能量球](#二、🍀使用自定义GLSL 着色器,生成漂流的3D 能量球)
-
- [1. ☘️实现思路](#1. ☘️实现思路)
- [2. ☘️代码样例](#2. ☘️代码样例)
一、🍀前言
本文详细介绍如何基于threejs在三维场景中自定义GLSL 着色器,生成漂流的3D 能量球,亲测可用。希望能帮助到您。一起学习,加油!加油!
1.1 ☘️GLSL着色器
GLSL(OpenGL Shading Language)是OpenGL的核心编程语言,用于编写图形渲染管线中可定制的计算逻辑。其核心设计目标是通过GPU并行计算实现高效的图形处理,支持从基础几何变换到复杂物理模拟的多样化需求。
1.1.1 ☘️着色器类型
顶点着色器(Vertex Shader)
- 功能:处理每个顶点的坐标变换(如模型视图投影矩阵变换)、法线计算及顶点颜色传递。
- 输出:裁剪空间坐标gl_Position,供后续光栅化阶段使用。
片段着色器(Fragment Shader)
- 功能:计算每个像素的最终颜色,支持纹理采样、光照模型(如Phong、PBR)及后处理效果(如模糊、景深)。
- 输出:像素颜色gl_FragColor或gl_FragColor(RGBA格式)。
计算着色器(Compute Shader,高级)
- 功能:执行通用并行计算任务(如物理模拟、图像处理),不直接绑定渲染管线。
- 特点:通过工作组(Work Group)实现高效数据并行处理。
1.1.2 ☘️工作原理
渲染管线流程
- 顶点处理:CPU提交顶点数据(位置、颜色、纹理坐标),GPU并行执行顶点着色器处理每个顶点。
- 光栅化:将顶点数据转换为像素片段,生成片段着色器输入。
- 片段处理:GPU并行执行片段着色器计算每个像素颜色。
- 输出合并:将片段颜色与帧缓冲区混合,生成最终图像。
数据流动
- 顶点属性:通过glVertexAttribPointer传递位置、颜色等数据,索引由layout(location=N)指定。
- Uniform变量:CPU通过glGetUniformLocation传递常量数据(如变换矩阵、时间),在渲染循环中更新。
- 内置变量: gl_Position(顶点着色器输出):裁剪空间坐标。 gl_FragCoord(片段着色器输入):当前像素的窗口坐标。
gl_FrontFacing(片段着色器输入):判断像素是否属于正面三角形。
1.1.3 ☘️核心特点
语法特性
- C语言变体:支持条件语句、循环、函数等结构,天然适配图形算法。
- 向量/矩阵运算:内置vec2/vec3/vec4及mat2/mat3/mat4类型,支持点乘、叉乘等操作。
- 精度限定符:如precision mediump float,控制计算精度与性能平衡。
硬件加速
- 并行计算:GPU数千个核心并行执行着色器代码,适合处理大规模数据(如粒子系统、体素渲染)。
- 内存模型:支持常量内存(Uniform)、纹理内存(Sampler)及共享内存(计算着色器),优化数据访问效率。
灵活性
- 可编程管线:完全替代固定渲染管线,支持自定义光照、阴影、后处理效果。
- 跨平台兼容性:OpenGL ES(移动端)与WebGL(Web)均支持GLSL,代码可移植性强。
1.1.4 ☘️应用场景
游戏开发
- 实时渲染:实现PBR材质、动态阴影、屏幕空间反射。
- 特效系统:粒子火焰、流体模拟、布料物理。
- 性能优化:通过计算着色器加速AI计算、碰撞检测。
数据可视化
- 科学计算:将多维数据映射为颜色/高度图(如气象数据、流场可视化)。
- 信息图表:动态生成3D柱状图、热力图,增强数据表现力。
艺术创作
- 程序化生成:使用噪声函数(如Perlin、Simplex)生成地形、纹理。
- 交互式装置:结合传感器数据实时修改着色器参数,创造动态艺术作品。
教育与研究
- 算法实验:实时调试光线追踪、路径追踪算法。
- 教学工具:可视化线性代数运算(如矩阵变换、向量投影)。
1.1.5 ☘️实战示例
顶点着色器(传递法线与世界坐标):
javascript
#version 330 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal; // 模型空间到世界空间的法线变换
gl_Position = projection * view * vec4(FragPos, 1.0);
}
片段着色器(实现Blinn-Phong光照):
javascript
#version 330 core
in vec3 FragPos;
in vec3 Normal;
out vec4 FragColor;
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;
void main() {
// 环境光
vec3 ambient = 0.1 * lightColor;
// 漫反射
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// 镜面反射
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = 0.5 * spec * lightColor;
// 最终颜色
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
}
二、🍀使用自定义GLSL 着色器,生成漂流的3D 能量球
1. ☘️实现思路
使用自定义GLSL 着色器定义THREE.ShaderMaterial材质material,定义THREE.SphereGeometry球体使用material材质生成漂流的3D 能量球。具体代码参考代码样例。可以直接运行。
2. ☘️代码样例
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3d能量球</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #050510;
font-family: sans-serif;
}
canvas {
display: block;
width: 100%;
height: 100%;
cursor: pointer;
}
#message-box {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 15px;
border-radius: 5px;
font-size: 14px;
display: block;
z-index: 10;
pointer-events: none;
}
</style>
</head>
<body>
<div id="message-box">Click/Tap the bubble for energy waves. Drag to rotate.</div>
<script type="importmap">
{
"imports": {
"three": "https://cdnjs.cloudflare.com/ajax/libs/three.js/0.163.0/three.module.min.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.163.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
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";
let scene, camera, renderer, bubble, innerCore, emissionBubble, clock, controls, particles;
let composer;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let isHovering = false;
const surfaceWaves = [];
const maxWaves = 5;
const lightningBranches = [];
const maxBranches = 15;
const particleCount = 5000;
let originalParticlePositions;
const simplexNoise3D = `
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec3 v) {
const vec2 C = vec2(1.0/6.0, 1.0/3.0); const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy)); vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz); vec3 l = 1.0 - g; vec3 i1 = min(g.xyz, l.zxy); vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx; vec3 x2 = x0 - i2 + C.yyy; vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857; vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z); vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ *ns.x + ns.yyyy; vec4 y = y_ *ns.x + ns.yyyy; vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy); vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0)*2.0 + 1.0; vec4 s1 = floor(b1)*2.0 + 1.0; vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy; vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;
vec3 p0 = vec3(a0.xy,h.x); vec3 p1 = vec3(a0.zw,h.y); vec3 p2 = vec3(a1.xy,h.z); vec3 p3 = vec3(a1.zw,h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); m = m * m;
return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
}
`;
const simplexNoise2D = `
vec2 mod289_2d(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 mod289_3d(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute_3d(vec3 x) { return mod289_3d(((x*34.0)+1.0)*x); }
float snoise2d(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy)); vec2 x0 = v - i + dot(i, C.xx);
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz; x12.xy -= i1;
i = mod289_2d(i);
vec3 p = permute_3d(permute_3d(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); m = m*m; m = m*m;
vec3 x = 2.0 * fract(p * C.www) - 1.0; vec3 h = abs(x) - 0.5; vec3 ox = floor(x + 0.5); vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
vec3 g; g.x = a0.x * x0.x + h.x * x0.y; g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
`;
function init() {
scene = new THREE.Scene();
clock = new THREE.Clock();
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 7;
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.body.appendChild(renderer.domElement);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 3;
controls.maxDistance = 25;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.15;
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.8);
directionalLight.position.set(5, 7, 5).normalize();
scene.add(directionalLight);
const pointLight = new THREE.PointLight(0xffccaa, 1.2, 150);
pointLight.position.set(-6, 4, -4);
scene.add(pointLight);
const cubeTextureLoader = new THREE.CubeTextureLoader();
const environmentMap = cubeTextureLoader.load(
[
"https://threejs.org/examples/textures/cube/Park3Med/px.jpg",
"https://threejs.org/examples/textures/cube/Park3Med/nx.jpg",
"https://threejs.org/examples/textures/cube/Park3Med/py.jpg",
"https://threejs.org/examples/textures/cube/Park3Med/ny.jpg",
"https://threejs.org/examples/textures/cube/Park3Med/pz.jpg",
"https://threejs.org/examples/textures/cube/Park3Med/nz.jpg",
],
() => {
scene.background = environmentMap;
scene.environment = environmentMap;
if (bubble) {
bubble.material.uniforms.envMap.value = environmentMap;
bubble.material.needsUpdate = true;
}
},
undefined,
(error) => {
console.error("Error loading environment map:", error);
scene.background = new THREE.Color(0x15151a);
const fallbackEnvMap = new THREE.CubeTexture();
scene.environment = fallbackEnvMap;
if (bubble) {
bubble.material.uniforms.envMap.value = fallbackEnvMap;
bubble.material.needsUpdate = true;
}
}
);
scene.background = new THREE.Color(0x15151a);
const bubbleVertexShader = `
uniform float time;
uniform vec2 waveOrigins[${maxWaves}];
uniform float waveStartTimes[${maxWaves}];
uniform float waveSpeeds[${maxWaves}];
uniform float waveAmplitudes[${maxWaves}];
varying vec3 vNormal;
varying vec3 vWorldNormal;
varying vec3 vPosition;
varying vec2 vUv;
varying vec3 vViewPosition;
varying float vWaveIntensity;
${simplexNoise3D}
void main() {
vUv = uv;
float noiseScale1=0.8; float noiseScale2=1.8; float noiseScale3=3.2;
float baseWobbleAmp=0.12; float mediumWobbleAmp=0.06; float rippleAmp=0.03;
vec3 baseWobblePos = position * noiseScale1 + vec3(time*0.15, time*0.12, time*0.20);
float baseWobble = snoise(baseWobblePos) * baseWobbleAmp;
vec3 mediumWobblePos = position * noiseScale2 + vec3(time*0.3, time*0.4, time*0.25);
float mediumWobble = snoise(mediumWobblePos) * mediumWobbleAmp;
vec3 ripplePos = position * noiseScale3 + vec3(time*0.6, time*0.7, time*0.5);
float ripple = snoise(ripplePos) * rippleAmp;
float deformation = baseWobble + mediumWobble + ripple;
float totalWaveDeformation = 0.0; vWaveIntensity = 0.0;
for(int i=0; i<${maxWaves}; i++) {
if(waveStartTimes[i] > 0.0) {
float waveTime = time - waveStartTimes[i];
if(waveTime > 0.0 && waveTime < 2.0) {
float dist = distance(uv, waveOrigins[i]);
float waveRadius = waveTime * waveSpeeds[i];
float waveFalloff = exp(-waveTime * 2.0);
float waveWidth = 0.1;
float wave = exp(-pow((dist - waveRadius) / waveWidth, 2.0)) * waveFalloff;
totalWaveDeformation += wave * waveAmplitudes[i] * sin(dist * 30.0 - waveTime * 15.0);
vWaveIntensity += wave * waveFalloff;
} } }
deformation += totalWaveDeformation * 0.2;
vec3 deformedNormal = normalize(normal);
vec3 newPosition = position + deformedNormal * deformation;
vec4 worldPosition = modelMatrix * vec4(newPosition, 1.0);
vPosition = worldPosition.xyz;
vWorldNormal = normalize(mat3(modelMatrix) * deformedNormal);
vNormal = normalize(normalMatrix * deformedNormal);
vec4 mvPosition = modelViewMatrix * vec4(newPosition, 1.0);
vViewPosition = -mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}`;
const bubbleFragmentShader = `
uniform samplerCube envMap; uniform float time; uniform float aberrationStrength;
uniform float iridescenceIntensity; uniform float u_hoverIntensity;
uniform vec2 u_crackleOriginUV; uniform float u_crackleStartTime; uniform float u_crackleDuration;
uniform vec3 u_crackleColor; uniform float u_crackleIntensity; uniform float u_crackleScale;
uniform float u_crackleSpeed; uniform float u_volumetricIntensity;
uniform vec2 u_branchOrigins[${maxBranches}]; uniform vec2 u_branchEnds[${maxBranches}];
uniform float u_branchStartTimes[${maxBranches}]; uniform float u_branchIntensities[${maxBranches}];
varying vec3 vNormal; varying vec3 vWorldNormal; varying vec3 vPosition;
varying vec2 vUv; varying vec3 vViewPosition; varying float vWaveIntensity;
${simplexNoise2D}
float cracklePattern(vec2 uv, float scale, float timeOffset) {
float flowNoise = snoise2d(uv * scale * 0.3 + vec2(timeOffset * 0.2));
vec2 flowDirection = vec2(cos(flowNoise * 2.0), sin(flowNoise * 2.0));
vec2 flowUV = uv + flowDirection * 0.02;
float n1 = snoise2d(flowUV * scale);
float n2 = snoise2d(flowUV * scale * 1.5 + vec2(timeOffset * 0.3));
float ridge1 = 1.0 - abs(n1);
float ridge2 = 1.0 - abs(n2 * 0.7);
float pattern = max(ridge1, ridge2);
pattern = smoothstep(0.85, 0.9, pattern);
float branches = abs(snoise2d(flowUV * scale * 3.0 - timeOffset));
branches = smoothstep(0.98, 0.99, branches);
pattern = max(pattern, branches * 0.5);
return smoothstep(0.4, 0.6, pattern);
}
float lightningBranch(vec2 uv, vec2 start, vec2 end, float thickness, float time) {
vec2 dir=end-start; float len=length(dir); if(len==0.0) return 0.0; vec2 norm=dir/len; vec2 perp=vec2(-norm.y,norm.x);
vec2 toPoint=uv-start; float alongLine=dot(toPoint,norm); float perpDist=abs(dot(toPoint,perp));
if(alongLine<0.0||alongLine>len) return 0.0; float noiseOffset=snoise2d(vec2(alongLine*10.0,time*3.0))*0.02;
perpDist-=noiseOffset; float intensity=exp(-perpDist*perpDist/(thickness*thickness)); return intensity; }
void main() {
vec3 viewDirection=normalize(vViewPosition); vec3 normal=normalize(vNormal); vec3 worldNormal=normalize(vWorldNormal);
vec3 worldViewDir=normalize(cameraPosition-vPosition); vec3 reflectDir=reflect(-worldViewDir,worldNormal);
float iorRatio=1.0/1.33; vec3 refractDirBase=refract(-worldViewDir,worldNormal,iorRatio);
vec3 aberrationOffset=worldNormal*aberrationStrength*0.05; vec3 refractDirR=normalize(refractDirBase+aberrationOffset);
vec3 refractDirG=refractDirBase; vec3 refractDirB=normalize(refractDirBase-aberrationOffset);
float refractR=textureCube(envMap,refractDirR).r; float refractG=textureCube(envMap,refractDirG).g; float refractB=textureCube(envMap,refractDirB).b;
vec3 refractedColorAberrated=vec3(refractR,refractG,refractB); vec4 reflectColor=textureCube(envMap,reflectDir);
float fresnelPower=4.0; float fresnelBase=0.06; float fresnel=fresnelBase+(1.0-fresnelBase)*pow(1.0-max(0.0,dot(viewDirection,normal)),fresnelPower);
fresnel=clamp(fresnel,0.0,1.0); float noiseScale=3.5; float n1=snoise2d(vUv*noiseScale+vec2(time*0.05))*0.5+0.5;
float n2=snoise2d(vUv*noiseScale*1.5+vec2(time*0.08+50.0))*0.5+0.5; float thicknessNoise=n1*n2;
float baseFilmThickness=350.0; float filmThicknessRange=450.0; float filmThickness=baseFilmThickness+thicknessNoise*filmThicknessRange;
vec3 wavelengths=vec3(700.0,530.0,440.0); vec3 interference=vec3(sin(filmThickness/wavelengths.r*20.0+time*0.5)*0.5+0.5,
sin(filmThickness/wavelengths.g*20.0+time*0.6)*0.5+0.5, sin(filmThickness/wavelengths.b*20.0+time*0.7)*0.5+0.5);
interference=pow(interference,vec3(1.5)); vec3 combinedColor=mix(refractedColorAberrated,reflectColor.rgb,fresnel);
combinedColor=mix(combinedColor,combinedColor*interference,iridescenceIntensity); float rimPower=3.0; float rimAmount=0.7;
float rim=rimAmount*pow(1.0-max(0.0,dot(viewDirection,normal)),rimPower); combinedColor+=vec3(rim*(0.8+u_hoverIntensity*0.4));
float crackleEmissionStrength=0.0; if(u_crackleStartTime>0.0){float crackleTime=time-u_crackleStartTime;if(crackleTime>=0.0&&crackleTime<u_crackleDuration){
float dist=distance(vUv,u_crackleOriginUV)*10.0; float currentRadius=crackleTime*u_crackleSpeed; if(dist<currentRadius){
float timeProgress=crackleTime/u_crackleDuration; float timeFalloff=smoothstep(1.0,0.5,timeProgress);
float patternValue=cracklePattern(vUv,u_crackleScale,time*0.8);
float distMask=smoothstep(currentRadius,currentRadius*0.5,dist);
float depth=length(vViewPosition); float volumetricFactor=1.0+u_volumetricIntensity*(1.0-exp(-depth*0.1));
crackleEmissionStrength=patternValue*u_crackleIntensity*timeFalloff*distMask*volumetricFactor;}}}
float lightningEmissionStrength=0.0; for(int i=0;i<${maxBranches};i++){if(u_branchStartTimes[i]>0.0){float branchTime=time-u_branchStartTimes[i];
float branchDuration=0.5; if(branchTime>0.0&&branchTime<branchDuration){float branchProgress=branchTime/branchDuration;
float branchFade=smoothstep(1.0,0.0,branchProgress); float branchIntensity=lightningBranch(vUv,u_branchOrigins[i],u_branchEnds[i],0.005,time);
lightningEmissionStrength+=branchIntensity*u_branchIntensities[i]*branchFade*2.0;}}}
vec3 waveGlow = u_crackleColor * vWaveIntensity * 0.2;
float patternOnly = step(0.7, crackleEmissionStrength + lightningEmissionStrength);
combinedColor += u_crackleColor * (crackleEmissionStrength + lightningEmissionStrength) * 0.05 * patternOnly + waveGlow;
float baseAlpha=0.4; float finalAlpha=mix(baseAlpha*0.5,baseAlpha,fresnel);
finalAlpha=clamp(finalAlpha+rim*0.1+(crackleEmissionStrength+lightningEmissionStrength)*0.1+vWaveIntensity*0.2,0.0,1.0);
gl_FragColor=vec4(combinedColor,finalAlpha);}`;
const emissionOnlyFragmentShader = `
uniform float time; uniform vec2 u_crackleOriginUV; uniform float u_crackleStartTime;
uniform float u_crackleDuration; uniform vec3 u_crackleColor; uniform float u_crackleIntensity;
uniform float u_crackleScale; uniform float u_crackleSpeed; uniform float u_volumetricIntensity;
uniform vec2 u_branchOrigins[${maxBranches}]; uniform vec2 u_branchEnds[${maxBranches}];
uniform float u_branchStartTimes[${maxBranches}]; uniform float u_branchIntensities[${maxBranches}];
varying vec2 vUv; varying vec3 vViewPosition;
${simplexNoise2D}
float cracklePattern(vec2 uv, float scale, float timeOffset) {
float n1=snoise2d(uv*scale+vec2(timeOffset*0.5));
float n2=snoise2d(uv*scale*2.1+vec2(-timeOffset*0.3,timeOffset*0.4)+10.0);
float n3=snoise2d(uv*scale*0.8+vec2(timeOffset*0.2,-timeOffset*0.6)-5.0);
float combined=abs(n1*0.5+n2*0.3+n3*0.2);
float pattern=pow(1.0-combined,40.0);
float sparks=snoise2d(uv*scale*5.0+timeOffset*2.0);
pattern+=pow(max(0.0,sparks),40.0)*0.1;
pattern = step(0.95, pattern);
return pattern;
}
float lightningBranch(vec2 uv, vec2 start, vec2 end, float thickness, float time) {
vec2 dir=end-start; float len=length(dir); if(len==0.0) return 0.0; vec2 norm=dir/len; vec2 perp=vec2(-norm.y,norm.x);
vec2 toPoint=uv-start; float alongLine=dot(toPoint,norm); float perpDist=abs(dot(toPoint,perp));
if(alongLine<0.0||alongLine>len) return 0.0; float noiseOffset=snoise2d(vec2(alongLine*10.0,time*3.0))*0.02;
perpDist-=noiseOffset; float intensity=exp(-perpDist*perpDist/(thickness*thickness)); return intensity; }
void main() {
float crackleEmissionStrength=0.0; if(u_crackleStartTime>0.0){float crackleTime=time-u_crackleStartTime;if(crackleTime>=0.0&&crackleTime<u_crackleDuration){
float dist=distance(vUv,u_crackleOriginUV)*10.0; float currentRadius=crackleTime*u_crackleSpeed; if(dist<currentRadius){
float timeProgress=crackleTime/u_crackleDuration; float timeFalloff=smoothstep(1.0,0.5,timeProgress);
float patternValue=cracklePattern(vUv,u_crackleScale,time*0.8);
float distMask=smoothstep(currentRadius,currentRadius*0.5,dist);
float depth=length(vViewPosition); float volumetricFactor=1.0+u_volumetricIntensity*(1.0-exp(-depth*0.1));
crackleEmissionStrength=patternValue*u_crackleIntensity*timeFalloff*distMask*volumetricFactor;}}}
float lightningEmissionStrength=0.0; for(int i=0;i<${maxBranches};i++){if(u_branchStartTimes[i]>0.0){float branchTime=time-u_branchStartTimes[i];
float branchDuration=0.5; if(branchTime>0.0&&branchTime<branchDuration){float branchProgress=branchTime/branchDuration;
float branchFade=smoothstep(1.0,0.0,branchProgress); float branchIntensity=lightningBranch(vUv,u_branchOrigins[i],u_branchEnds[i],0.005,time);
lightningEmissionStrength+=branchIntensity*u_branchIntensities[i]*branchFade*2.0;}}}
float totalEmissionStrength = crackleEmissionStrength + lightningEmissionStrength;
float emissionBoost = 8.0;
vec3 finalColor = u_crackleColor * totalEmissionStrength * emissionBoost;
gl_FragColor = vec4(finalColor, step(0.9, totalEmissionStrength));
}`;
const coreVertexShader = `
uniform float time; uniform float noiseScale; uniform float noiseAmplitude; varying float vNoise;
${simplexNoise3D}
void main() {float noise=snoise(position*noiseScale+vec3(time*0.3)); vNoise=noise;
vec3 displacedPosition=position+normal*noise*noiseAmplitude; gl_Position=projectionMatrix*modelViewMatrix*vec4(displacedPosition,1.0);}`;
const coreFragmentShader = `
uniform float time; uniform vec3 baseColor; uniform float opacityFactor; varying float vNoise;
void main() {float colorIntensity=smoothstep(-1.0,1.0,vNoise)*0.6+0.8; vec3 dynamicColor=baseColor*colorIntensity;
float pulse=sin(time*2.5+vNoise*2.0)*0.5+0.5; float noiseOpacity=smoothstep(-0.6,0.2,vNoise);
float finalOpacity=noiseOpacity*pulse*opacityFactor; gl_FragColor=vec4(dynamicColor,finalOpacity);}`;
const particleData = createReactiveParticleSystem();
particles = particleData.particles;
originalParticlePositions = particleData.originalPositions;
scene.add(particles);
const bubbleGeometry = new THREE.SphereGeometry(2, 128, 128);
const bubbleMaterial = new THREE.ShaderMaterial({
vertexShader: bubbleVertexShader,
fragmentShader: bubbleFragmentShader,
uniforms: THREE.UniformsUtils.clone({
envMap: { value: scene.environment || new THREE.CubeTexture() },
time: { value: 0 },
aberrationStrength: { value: 0.8 },
iridescenceIntensity: { value: 0.6 },
u_hoverIntensity: { value: 0.0 },
u_crackleOriginUV: { value: new THREE.Vector2(0.5, 0.5) },
u_crackleStartTime: { value: -1.0 },
u_crackleDuration: { value: 1.5 },
u_crackleColor: { value: new THREE.Color(0.9, 0.95, 1.0) },
u_crackleIntensity: { value: 1.5 },
u_crackleScale: { value: 25.0 },
u_crackleSpeed: { value: 8.0 },
u_volumetricIntensity: { value: 0.05 },
waveOrigins: {
value: Array(maxWaves)
.fill()
.map(() => new THREE.Vector2(0, 0)),
},
waveStartTimes: { value: Array(maxWaves).fill(-1) },
waveSpeeds: { value: Array(maxWaves).fill(1.0) },
waveAmplitudes: { value: Array(maxWaves).fill(0.1) },
u_branchOrigins: {
value: Array(maxBranches)
.fill()
.map(() => new THREE.Vector2(0, 0)),
},
u_branchEnds: {
value: Array(maxBranches)
.fill()
.map(() => new THREE.Vector2(0, 0)),
},
u_branchStartTimes: { value: Array(maxBranches).fill(-1) },
u_branchIntensities: { value: Array(maxBranches).fill(1.0) },
}),
transparent: true,
side: THREE.DoubleSide,
depthWrite: false,
});
bubble = new THREE.Mesh(bubbleGeometry, bubbleMaterial);
scene.add(bubble);
const emissionOnlyMaterial = new THREE.ShaderMaterial({
vertexShader: bubbleVertexShader,
fragmentShader: emissionOnlyFragmentShader,
uniforms: bubbleMaterial.uniforms,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
emissionBubble = new THREE.Mesh(bubbleGeometry, emissionOnlyMaterial);
scene.add(emissionBubble);
const coreGeometry = new THREE.SphereGeometry(0.6, 64, 64);
const coreMaterial = new THREE.ShaderMaterial({
vertexShader: coreVertexShader,
fragmentShader: coreFragmentShader,
uniforms: {
time: { value: 0.0 },
noiseScale: { value: 2.5 },
noiseAmplitude: { value: 0.25 },
baseColor: { value: new THREE.Color(0x99bbff) },
opacityFactor: { value: 0.85 },
},
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
innerCore = new THREE.Mesh(coreGeometry, coreMaterial);
scene.add(innerCore);
setupPostProcessing();
window.addEventListener("resize", onWindowResize);
renderer.domElement.addEventListener("mousedown", onMouseDown);
renderer.domElement.addEventListener("mousemove", onMouseMove);
}
function createParticleTexture() {
const canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 64;
const context = canvas.getContext("2d");
const gradient = context.createRadialGradient(32, 32, 0, 32, 32, 32);
gradient.addColorStop(0, "rgba(255,255,255,1)");
gradient.addColorStop(0.2, "rgba(255,255,255,0.8)");
gradient.addColorStop(0.6, "rgba(200,200,255,0.4)");
gradient.addColorStop(1, "rgba(150,150,255,0)");
context.fillStyle = gradient;
context.fillRect(0, 0, 64, 64);
return new THREE.CanvasTexture(canvas);
}
function createReactiveParticleSystem() {
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
const velocities = new Float32Array(particleCount * 3);
const radius = 15;
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
const u = Math.random();
const v = Math.random();
const theta = u * 2.0 * Math.PI;
const phi = Math.acos(2.0 * v - 1.0);
const r = Math.cbrt(Math.random()) * radius;
positions[i3] = r * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = r * Math.sin(phi) * Math.sin(theta);
positions[i3 + 2] = r * Math.cos(phi);
const colorVariance = Math.random() * 0.3;
colors[i3] = 1.0 - colorVariance * 0.5;
colors[i3 + 1] = 1.0 - colorVariance * 0.5;
colors[i3 + 2] = 1.0;
velocities[i3] = (Math.random() - 0.5) * 0.02;
velocities[i3 + 1] = (Math.random() - 0.5) * 0.02;
velocities[i3 + 2] = (Math.random() - 0.5) * 0.02;
}
const particleGeometry = new THREE.BufferGeometry();
particleGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
particleGeometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
const particleTexture = createParticleTexture();
const particleMaterial = new THREE.PointsMaterial({
size: 0.12,
map: particleTexture,
vertexColors: true,
transparent: true,
opacity: 0.7,
blending: THREE.AdditiveBlending,
depthWrite: false,
sizeAttenuation: true,
});
const particles = new THREE.Points(particleGeometry, particleMaterial);
particles.userData.velocities = velocities;
return { particles: particles, originalPositions: new Float32Array(positions) };
}
function setupPostProcessing() {
composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.1, 0.3, 0.97);
composer.addPass(bloomPass);
const colorGradingShader = {
uniforms: { tDiffuse: { value: null }, contrast: { value: 1.15 }, brightness: { value: 0.03 }, saturation: { value: 1.2 } },
vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
fragmentShader: ` uniform sampler2D tDiffuse; uniform float contrast; uniform float brightness; uniform float saturation; varying vec2 vUv;
vec3 adjustSaturation(vec3 color, float adjustment) { vec3 gray = vec3(dot(color, vec3(0.299, 0.587, 0.114))); return mix(gray, color, adjustment); }
void main() { vec4 color = texture2D(tDiffuse, vUv); color.rgb = adjustSaturation(color.rgb, saturation); color.rgb = (color.rgb - 0.5) * contrast + 0.5 + brightness; gl_FragColor = clamp(color, 0.0, 1.0); }`,
};
const colorGradingPass = new ShaderPass(colorGradingShader);
composer.addPass(colorGradingPass);
}
function onMouseDown(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(bubble);
if (intersects.length > 0) {
const intersection = intersects[0];
const uv = intersection.uv;
bubble.material.uniforms.u_crackleOriginUV.value.copy(uv);
bubble.material.uniforms.u_crackleStartTime.value = clock.getElapsedTime();
addSurfaceWave(uv);
generateLightningBranches(uv);
}
}
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(bubble);
isHovering = intersects.length > 0;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
function addSurfaceWave(uv) {
const waveIndex = surfaceWaves.length % maxWaves;
const uniforms = bubble.material.uniforms;
uniforms.waveOrigins.value[waveIndex].copy(uv);
uniforms.waveStartTimes.value[waveIndex] = clock.getElapsedTime();
uniforms.waveSpeeds.value[waveIndex] = 0.8 + Math.random() * 0.4;
uniforms.waveAmplitudes.value[waveIndex] = 0.08 + Math.random() * 0.04;
surfaceWaves.push({ index: waveIndex, startTime: clock.getElapsedTime() });
}
function generateLightningBranches(origin) {
const branchCount = 1 + Math.floor(Math.random() * 3);
const uniforms = bubble.material.uniforms;
for (let i = 0; i < branchCount; i++) {
const branchIndex = lightningBranches.length % maxBranches;
const angle = Math.random() * Math.PI * 2;
const length = 0.1 + Math.random() * 0.3;
uniforms.u_branchOrigins.value[branchIndex].copy(origin);
uniforms.u_branchEnds.value[branchIndex].set(origin.x + Math.cos(angle) * length, origin.y + Math.sin(angle) * length);
uniforms.u_branchStartTimes.value[branchIndex] = clock.getElapsedTime() + Math.random() * 0.2;
uniforms.u_branchIntensities.value[branchIndex] = 0.5 + Math.random() * 0.5;
lightningBranches.push({ index: branchIndex });
}
}
function updateParticles(time, deltaTime) {
const positions = particles.geometry.attributes.position.array;
const velocities = particles.userData.velocities;
const bubblePosition = bubble.position;
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
let x = positions[i3];
let y = positions[i3 + 1];
let z = positions[i3 + 2];
const dx = x - bubblePosition.x;
const dy = y - bubblePosition.y;
const dz = z - bubblePosition.z;
const distSq = dx * dx + dy * dy + dz * dz;
const dist = Math.sqrt(distSq);
if (bubble.material.uniforms.u_crackleStartTime.value > 0) {
const crackleTime = time - bubble.material.uniforms.u_crackleStartTime.value;
if (crackleTime > 0 && crackleTime < 1.5 && dist > 0) {
const repelForce = 0.5 * (1 - crackleTime / 1.5);
const invDist = 1.0 / dist;
velocities[i3] += dx * invDist * repelForce * deltaTime;
velocities[i3 + 1] += dy * invDist * repelForce * deltaTime;
velocities[i3 + 2] += dz * invDist * repelForce * deltaTime;
}
}
const attractionForce = 0.1;
if (dist > 3 && dist > 0) {
const invDist = 1.0 / dist;
velocities[i3] -= dx * invDist * attractionForce * deltaTime;
velocities[i3 + 1] -= dy * invDist * attractionForce * deltaTime;
velocities[i3 + 2] -= dz * invDist * attractionForce * deltaTime;
}
positions[i3] += velocities[i3];
positions[i3 + 1] += velocities[i3 + 1];
positions[i3 + 2] += velocities[i3 + 2];
velocities[i3] *= 0.98;
velocities[i3 + 1] *= 0.98;
velocities[i3 + 2] *= 0.98;
}
particles.geometry.attributes.position.needsUpdate = true;
}
function animate() {
requestAnimationFrame(animate);
const elapsedTime = clock.getElapsedTime();
const deltaTime = clock.getDelta();
if (bubble) {
bubble.material.uniforms.time.value = elapsedTime;
const targetHover = isHovering ? 1.0 : 0.0;
bubble.material.uniforms.u_hoverIntensity.value += (targetHover - bubble.material.uniforms.u_hoverIntensity.value) * 0.1;
}
if (innerCore) {
innerCore.material.uniforms.time.value = elapsedTime;
}
updateParticles(elapsedTime, deltaTime);
controls.update();
composer.render();
}
window.onload = () => {
try {
init();
animate();
setTimeout(() => {
const msgBox = document.getElementById("message-box");
if (msgBox) msgBox.style.display = "none";
}, 5000);
} catch (error) {
console.error("Initialization or Animation Error:", error);
const msgBox = document.getElementById("message-box");
if (msgBox) {
msgBox.textContent = "Error initializing simulation. Check console.";
msgBox.style.backgroundColor = "red";
msgBox.style.display = "block";
}
}
};
</script>
</body>
</html>
效果如下
参考:源码