学习threejs,使用自定义GLSL 着色器,生成漂流的3D能量球

👨‍⚕️ 主页: gis分享者

👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!

👨‍⚕️ 收录于专栏:threejs 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>

效果如下

参考:源码

相关推荐
m0_743106466 小时前
【论文笔记】BlockGaussian:巧妙解决大规模场景重建中的伪影问题
论文阅读·计算机视觉·3d·aigc·几何学
向宇it11 小时前
【unity小技巧】在 Unity 中将 2D 精灵添加到 3D 游戏中,并实现阴影投射效果,实现类《八分旅人》《饥荒》等等的2.5D游戏效果
游戏·3d·unity·编辑器·游戏引擎·材质
荔枝味啊~1 天前
相机位姿估计
人工智能·计算机视觉·3d
在下胡三汉2 天前
什么是 3D 文件?
3d
点云登山者2 天前
登山第二十六梯:单目3D检测一切——一只眼看世界
3d·3d检测·检测一切·单目3d检测
xhload3d3 天前
智慧航天运载体系全生命周期监测 | 图扑数字孪生
物联网·3d·智慧城市·html5·webgl·数字孪生·可视化·工业互联网·三维建模·工控·航空航天·火箭升空·智慧航空·智慧航天·火箭发射·火箭回收
小赖同学啊3 天前
光伏园区3d系统管理
前端·javascript·3d
魂断蓝桥6663 天前
使用three.js,实现微信3D小游戏系列教程,框架篇(一)
webgl·threejs·微信小游戏·3d建筑·three.js路径规划、三维a*算法、javascript三维导航,·three.js小游戏
SDUERPANG3 天前
三维目标检测|Iou3D 代码解读一
人工智能·目标检测·3d