👨??? 主頁: gis分享者
👨??? 感謝各位大佬 點贊👍 收藏? 留言📝 加關注?!
👨??? 收錄于專欄:threejs gis工程師
文章目錄
- 一、🍀前言
- 1.1 ??GLSL著色器
- 1.1.1 ??著色器類型
- 1.1.2 ??工作原理
- 1.1.3 ??核心特點
- 1.1.4 ??應用場景
- 1.1.5 ??實戰示例
- 二、🍀使用自定義GLSL 著色器,生成漂流的3D 能量球
- 1. ??實現思路
- 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 ??實戰示例
頂點著色器(傳遞法線與世界坐標):
#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光照):
#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. ??代碼樣例
<!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>
效果如下
參考:源碼