👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨⚕️ 收录于专栏:threejs gis工程师
文章目录
- 一、🍀前言
- 1.1 ☘️THREE.EffectComposer 后期处理
- 1.1.1 ☘️代码示例
- 1.1.2 ☘️构造函数
- 1.1.3 ☘️属性
- 1.1.4 ☘️方法
- 1.2 ☘️THREE.RenderPass
- 1.2.1 ☘️构造函数
- 1.2.2 ☘️属性
- 1.2.3 ☘️方法
- 1.3 ☘️THREE.UnrealBloomPass
- 1.3.1 ☘️构造函数
- 1.3.2 ☘️方法
- 1.4 ☘️THREE.FilmPass
- 1.4.1 ☘️构造函数
- 1.4.2 ☘️属性
- 1.4.3 ☘️方法
- 二、🍀使用EffectComposer后期处理组合器(采用RenderPass、UnrealBloomPass、FilmPass渲染通道),实现交互式 3D blob
- 1. ☘️实现思路
- 2. ☘️代码样例
一、🍀前言
本文详细介绍如何基于threejs在三维场景中使用EffectComposer后期处理组合器(采用RenderPass、UnrealBloomPass、FilmPass渲染通道),实现交互式 3D blob,亲测可用。希望能帮助到您。一起学习,加油!加油!
1.1 ☘️THREE.EffectComposer 后期处理
THREE.EffectComposer 用于在three.js中实现后期处理效果。该类管理了产生最终视觉效果的后期处理过程链。 后期处理过程根据它们添加/插入的顺序来执行,最后一个过程会被自动渲染到屏幕上。
1.1.1 ☘️代码示例
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
// 初始化 composer
const composer = new EffectComposer(renderer);
// 创建 RenderPass 并添加到 composer
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 添加其他后期处理通道(如模糊)
// composer.addPass(blurPass);
// 在动画循环中渲染
function animate() {composer.render();requestAnimationFrame(animate);
}
1.1.2 ☘️构造函数
EffectComposer( renderer : WebGLRenderer, renderTarget : WebGLRenderTarget )
renderer – 用于渲染场景的渲染器。
renderTarget – (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。
1.1.3 ☘️属性
.passes : Array
一个用于表示后期处理过程链(包含顺序)的数组。
渲染通道:
BloomPass 该通道会使得明亮区域参入较暗的区域。模拟相机照到过多亮光的情形
DotScreenPass 将一层黑点贴到代表原始图片的屏幕上
FilmPass 通过扫描线和失真模拟电视屏幕
MaskPass 在当前图片上贴一层掩膜,后续通道只会影响被贴的区域
RenderPass 该通道在指定的场景和相机的基础上渲染出一个新的场景
SavePass 执行该通道时,它会将当前渲染步骤的结果复制一份,方便后面使用。这个通道实际应用中作用不大;
ShaderPass 使用该通道你可以传入一个自定义的着色器,用来生成高级的、自定义的后期处理通道
TexturePass 该通道可以将效果组合器的当前状态保存为一个纹理,然后可以在其他EffectCoposer对象中将该纹理作为输入参数
.readBuffer : WebGLRenderTarget
内部读缓冲区的引用。过程一般从该缓冲区读取先前的渲染结果。
.renderer : WebGLRenderer
内部渲染器的引用。
.renderToScreen : Boolean
最终过程是否被渲染到屏幕(默认帧缓冲区)。
.writeBuffer : WebGLRenderTarget
内部写缓冲区的引用。过程常将它们的渲染结果写入该缓冲区。
1.1.4 ☘️方法
.addPass ( pass : Pass ) : undefined
pass – 将被添加到过程链的过程
将传入的过程添加到过程链。
.dispose () : undefined
释放此实例分配的 GPU 相关资源。每当您的应用程序不再使用此实例时调用此方法。
.insertPass ( pass : Pass, index : Integer ) : undefined
pass – 将被插入到过程链的过程。
index – 定义过程链中过程应插入的位置。
将传入的过程插入到过程链中所给定的索引处。
.isLastEnabledPass ( passIndex : Integer ) : Boolean
passIndex – 被用于检查的过程
如果给定索引的过程在过程链中是最后一个启用的过程,则返回true。 由EffectComposer所使用,来决定哪一个过程应当被渲染到屏幕上。
.removePass ( pass : Pass ) : undefined
pass – 要从传递链中删除的传递。
从传递链中删除给定的传递。
.render ( deltaTime : Float ) : undefined
deltaTime – 增量时间值。
执行所有启用的后期处理过程,来产生最终的帧,
.reset ( renderTarget : WebGLRenderTarget ) : undefined
renderTarget – (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。
重置所有EffectComposer的内部状态。
.setPixelRatio ( pixelRatio : Float ) : undefined
pixelRatio – 设备像素比
设置设备的像素比。该值通常被用于HiDPI设备,以阻止模糊的输出。 因此,该方法语义类似于WebGLRenderer.setPixelRatio()。
.setSize ( width : Integer, height : Integer ) : undefined
width – EffectComposer的宽度。
height – EffectComposer的高度。
考虑设备像素比,重新设置内部渲染缓冲和过程的大小为(width, height)。 因此,该方法语义类似于WebGLRenderer.setSize()。
.swapBuffers () : undefined
交换内部的读/写缓冲。
1.2 ☘️THREE.RenderPass
THREE.RenderPass用于将场景渲染到中间缓冲区,为后续的后期处理效果(如模糊、色调调整等)提供基础。
1.2.1 ☘️构造函数
RenderPass(scene, camera, overrideMaterial, clearColor, clearAlpha)
- scene THREE.Scene 要渲染的 Three.js 场景对象。
- camera THREE.Camera 场景对应的相机(如 PerspectiveCamera)。
- overrideMaterial THREE.Material (可选) 覆盖场景中所有物体的材质(默认 null)。
- clearColor THREE.Color (可选) 渲染前清除画布的颜色(默认不主动清除)。
- clearAlpha number (可选) 清除画布的透明度(默认 0)。
1.2.2 ☘️属性
.enabled:boolean
是否启用此通道(默认 true)。设为 false 可跳过渲染。
.clear:boolean
渲染前是否清除画布(默认 true)。若需叠加多个 RenderPass,可设为 false。
.needsSwap:boolean
是否需要在渲染后交换缓冲区(通常保持默认 false)。
1.2.3 ☘️方法
.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。
1.3 ☘️THREE.UnrealBloomPass
UnrealBloomPass 是 Three.js 中实现高质量泛光效果的后期处理通道,通过模拟类似 Unreal Engine 的泛光效果,为场景中的明亮区域添加柔和的光晕,提升视觉表现力。
1.3.1 ☘️构造函数
new UnrealBloomPass(resolution, strength, radius, threshold)
- resolution (Vector2): 泛光效果应用的场景分辨率,需与画布尺寸一致。
示例:new THREE.Vector2(window.innerWidth, window.innerHeight) - strength (Number): 泛光强度,默认值 1.0。值越大,光晕越明显。
- radius (Number): 模糊半径,默认值 0.4。值越大,光晕扩散范围越广。
- threshold (Number): 泛光阈值,默认值 0.85。仅对亮度高于此值的区域生效。
1.3.2 ☘️方法
- renderToScreen: 是否直接渲染到屏幕,默认为 false(需通过 EffectComposer 管理)。
- clearColor: 设置背景清除颜色,默认为透明。
1.4 ☘️THREE.FilmPass
THREE.FilmPass是 Three.js 后期处理模块中的一个特效通道,用于模拟电影胶片效果(如扫描线、颗粒噪声和画面抖动)。适用于复古风格或科幻场景的视觉增强。
1.4.1 ☘️构造函数
FilmPass(
noiseIntensity, // 噪声强度
scanlinesIntensity,// 扫描线强度
scanlinesCount, // 扫描线数量
grayscale // 是否转为灰度
)
1.4.2 ☘️属性
.enabled:boolean
是否启用此通道(默认 true)。设为 false 可临时禁用效果。
.uniforms:object
着色器 uniforms 对象,可直接修改参数(动态调整效果):
filmPass.uniforms.nIntensity.value = 0.8; // 调整噪声强度
filmPass.uniforms.sIntensity.value = 0.5; // 调整扫描线强度
filmPass.uniforms.sCount.value = 1024; // 调整扫描线密度
filmPass.uniforms.grayscale.value = 1; // 启用灰度(1 是,0 否)
1.4.3 ☘️方法
.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。
二、🍀使用EffectComposer后期处理组合器(采用RenderPass、UnrealBloomPass、FilmPass渲染通道),实现交互式 3D blob
1. ☘️实现思路
本例子使用EffectComposer后期处理组合器(采用RenderPass、UnrealBloomPass、FilmPass渲染通道)、THREE.Points点对象等,实现交互式 3D blob。具体代码参考下面代码样例。
2. ☘️代码样例
<!DOCTYPE html>
<html lang="en">
<head><script src="https://cdn.tailwindcss.com"></script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>交互式 3D blob</title><style>body {margin: 0;overflow: hidden;font-family: 'Inter', sans-serif;}canvas {display: block;width: 100vw;height: 100vh;}::-webkit-scrollbar {width: 8px;}::-webkit-scrollbar-track {background: rgba(255, 255, 255, 0.1);border-radius: 10px;}::-webkit-scrollbar-thumb {background: rgba(255, 255, 255, 0.3);border-radius: 10px;}::-webkit-scrollbar-thumb:hover {background: rgba(255, 255, 255, 0.5);}select {background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23DDDDDD%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");background-repeat: no-repeat;background-position: right 0.75rem top 50%;background-size: 0.65em auto;-webkit-appearance: none;-moz-appearance: none;appearance: none;}</style>
</head>
<body>
<div id="ui-container" class="fixed top-5 right-5 z-50 bg-gray-900/70 backdrop-blur-md p-6 rounded-xl shadow-2xl border border-gray-700/50 transition-all duration-300 ease-in-out w-72"><h3 class="text-lg font-semibold text-gray-100 mb-5 tracking-wide uppercase">Dithering Controls</h3><div class="mb-5"><label for="dither-pattern" class="block text-sm font-medium text-gray-300 mb-2">Dither Pattern</label><select id="dither-pattern" class="w-full bg-gray-800/80 border border-gray-700 text-gray-200 text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 block p-2.5 placeholder-gray-400 shadow-sm appearance-none"><option value="0">Bayer Matrix (8x8)</option><option value="1">Halftone Dots</option><option value="2">Line Pattern</option><option value="3">Noise Dithering</option><option value="4">No Dithering</option></select></div><div class="mb-3"><label for="dither-scale" class="block text-sm font-medium text-gray-300 mb-2">Dither Scale</label><select id="dither-scale" class="w-full bg-gray-800/80 border border-gray-700 text-gray-200 text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 block p-2.5 placeholder-gray-400 shadow-sm appearance-none"><option value="1.0">Fine</option><option value="1.5" selected>Medium</option><option value="2.5">Coarse</option><option value="3.5">Very Coarse</option></select></div>
</div>
</body>
<script type="module">import * as THREE from "https://esm.sh/three";import { OrbitControls } from "https://esm.sh/three/examples/jsm/controls/OrbitControls.js";import { EffectComposer } from "https://esm.sh/three/examples/jsm/postprocessing/EffectComposer.js";import { RenderPass } from "https://esm.sh/three/examples/jsm/postprocessing/RenderPass.js";import { UnrealBloomPass } from "https://esm.sh/three/examples/jsm/postprocessing/UnrealBloomPass.js";import { FilmPass } from "https://esm.sh/three/examples/jsm/postprocessing/FilmPass.js";const DITHER_MOTION_SPEED = 2.0;const DITHER_MOTION_AMPLITUDE = 1.5;const BLOB_BASE_RADIUS = 2.0;const BLOB_NOISE_FREQUENCY_VERTEX = 0.75;const BLOB_NOISE_AMPLITUDE_VERTEX = 0.65;const BLOB_NOISE_SPEED_VERTEX = 0.08;const PARTICLE_COUNT = 1200;const STAR_COUNT = 3000;const scene = new THREE.Scene();scene.background = new THREE.Color(0x000000);scene.fog = new THREE.FogExp2(0x000000, 0.025);const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);camera.position.set(0, 0, 5.0);const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });renderer.setSize(window.innerWidth, window.innerHeight);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));renderer.outputColorSpace = THREE.SRGBColorSpace;document.body.appendChild(renderer.domElement);const composer = new EffectComposer(renderer);const renderPass = new RenderPass(scene, camera);composer.addPass(renderPass);const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.45, 0.55, 0.75);composer.addPass(bloomPass);const filmPass = new FilmPass(0.20, 0.15, 648, false);composer.addPass(filmPass);const controls = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true;controls.dampingFactor = 0.04;controls.rotateSpeed = 0.20;controls.minDistance = 2.0;controls.maxDistance = 12;controls.enablePan = false;controls.autoRotate = false;const ambientLight = new THREE.AmbientLight(0x606070, 0.6);scene.add(ambientLight);const pointLight1 = new THREE.PointLight(0xffddaa, 0.9, 60);pointLight1.position.set(5, 5, 5);scene.add(pointLight1);const pointLight2 = new THREE.PointLight(0xaaccff, 0.6, 60);pointLight2.position.set(-5, -3, -4);scene.add(pointLight2);const pointLight3 = new THREE.PointLight(0xff8844, 0.75, 60);pointLight3.position.set(0, -5, 3);scene.add(pointLight3);const starGeometry = new THREE.BufferGeometry();const starPositions = [];const starColors = [];const starSizes = [];for (let i = 0; i < STAR_COUNT; i++) {const x = THREE.MathUtils.randFloatSpread(200);const y = THREE.MathUtils.randFloatSpread(200);const z = THREE.MathUtils.randFloatSpread(200);starPositions.push(x, y, z);const color = new THREE.Color();color.setHSL(THREE.MathUtils.randFloat(0.5, 0.7), 0.2, THREE.MathUtils.randFloat(0.3, 0.6));starColors.push(color.r, color.g, color.b);starSizes.push(THREE.MathUtils.randFloat(0.5, 1.5));}starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starPositions, 3));starGeometry.setAttribute('color', new THREE.Float32BufferAttribute(starColors, 3));starGeometry.setAttribute('size', new THREE.Float32BufferAttribute(starSizes, 1));const starMaterial = new THREE.ShaderMaterial({uniforms: {uTime: { value: 0.0 },},vertexShader: `uniform float uTime;attribute float size;varying vec3 vColor;varying float vAlpha;void main() {vColor = color;vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);gl_PointSize = size * (100.0 / -mvPosition.z) * (sin(position.x * 0.1 + uTime * 0.3) * 0.2 + 0.8);vAlpha = clamp(1.0 - (-mvPosition.z / 150.0), 0.1, 0.8);gl_Position = projectionMatrix * mvPosition;}`,fragmentShader: `uniform float uTime;varying vec3 vColor;varying float vAlpha;void main() {float dist = length(gl_PointCoord - vec2(0.5));if (dist > 0.5) discard;gl_FragColor = vec4(vColor, vAlpha * (0.6 + 0.4 * sin(uTime * 2.0 + gl_FragCoord.x * 0.5)));}`,transparent: true,blending: THREE.AdditiveBlending,depthWrite: false,vertexColors: true});const stars = new THREE.Points(starGeometry, starMaterial);scene.add(stars);const ditherPatternsFunction = `const float bayerMatrix[64] = float[64](0.0/64.0, 32.0/64.0, 8.0/64.0, 40.0/64.0, 2.0/64.0, 34.0/64.0, 10.0/64.0, 42.0/64.0,48.0/64.0, 16.0/64.0, 56.0/64.0, 24.0/64.0, 50.0/64.0, 18.0/64.0, 58.0/64.0, 26.0/64.0,12.0/64.0, 44.0/64.0, 4.0/64.0, 36.0/64.0, 14.0/64.0, 46.0/64.0, 6.0/64.0, 38.0/64.0,60.0/64.0, 28.0/64.0, 52.0/64.0, 20.0/64.0, 62.0/64.0, 30.0/64.0, 54.0/64.0, 22.0/64.0,3.0/64.0, 35.0/64.0, 11.0/64.0, 43.0/64.0, 1.0/64.0, 33.0/64.0, 9.0/64.0, 41.0/64.0,51.0/64.0, 19.0/64.0, 59.0/64.0, 27.0/64.0, 49.0/64.0, 17.0/64.0, 57.0/64.0, 25.0/64.0,15.0/64.0, 47.0/64.0, 7.0/64.0, 39.0/64.0, 13.0/64.0, 45.0/64.0, 5.0/64.0, 37.0/64.0,63.0/64.0, 31.0/64.0, 55.0/64.0, 23.0/64.0, 61.0/64.0, 29.0/64.0, 53.0/64.0, 21.0/64.0);float getBayerValue(vec2 coord) {int x = int(mod(coord.x, 8.0));int y = int(mod(coord.y, 8.0));return bayerMatrix[y * 8 + x];}float getHalftoneValue(vec2 coord, float time) {vec2 c = vec2(0.5);coord = mod(coord * 0.1 + vec2(sin(time*0.1)*0.02, cos(time*0.1)*0.02), 1.0);float d = distance(coord, c);return smoothstep(0.28, 0.29, d);}float getLinePatternValue(vec2 coord, float time) {float lw = 0.35 + sin(time*0.15)*0.1;float p1 = mod(coord.x*0.15+sin(coord.y*0.04+time*0.08)*0.6,1.0);float p2 = mod(coord.y*0.15+cos(coord.x*0.04+time*0.12)*0.6,1.0);return max(smoothstep(0.0,lw,p1)*smoothstep(1.0,1.0-lw,p1), smoothstep(0.0,lw,p2)*smoothstep(1.0,1.0-lw,p2));}float getNoiseDitheringValue(vec2 coord, float time) {return fract(sin(dot(coord + time * 0.05, vec2(12.9898, 78.233))) * 43758.5453);}vec3 ditherMonochrome(vec3 color, vec2 baseScreenPos, float colorLevels, float time,float motionSpeed, float motionAmplitude, int patternType) {float luminance = dot(color, vec3(0.299, 0.587, 0.114));luminance = pow(luminance, 1.2);luminance = (luminance - 0.5) * 6.0 + 0.5;luminance = clamp(luminance, 0.0, 1.0);vec2 ditherScreenPos = baseScreenPos;ditherScreenPos.x += sin(time * motionSpeed * 0.75 + baseScreenPos.y * 0.08) * motionAmplitude;ditherScreenPos.y += cos(time * motionSpeed * 0.55 + baseScreenPos.x * 0.08) * motionAmplitude;float threshold = 0.5;if (patternType == 0) {threshold = getBayerValue(ditherScreenPos);} else if (patternType == 1) {threshold = getHalftoneValue(ditherScreenPos, time);} else if (patternType == 2) {threshold = getLinePatternValue(ditherScreenPos, time);} else if (patternType == 3) {threshold = getNoiseDitheringValue(ditherScreenPos, time);} else if (patternType == 4) {threshold = 0.5;}float ditheredValue = (luminance < threshold) ? 0.0 : 1.0;return vec3(ditheredValue);}
`;const glslRandFunction = `float rand(vec3 co){ return fract(sin(dot(co, vec3(12.9898,78.233,53.543))) * 43758.5453); }float snoise(vec3 p) {vec3 ip = floor(p); vec3 fp = fract(p); fp = fp*fp*(3.0-2.0*fp);float v000=rand(ip+vec3(0,0,0)); float v100=rand(ip+vec3(1,0,0)); float v010=rand(ip+vec3(0,1,0)); float v110=rand(ip+vec3(1,1,0));float v001=rand(ip+vec3(0,0,1)); float v101=rand(ip+vec3(1,0,1)); float v011=rand(ip+vec3(0,1,1)); float v111=rand(ip+vec3(1,1,1));return mix(mix(mix(v000,v100,fp.x),mix(v010,v110,fp.x),fp.y), mix(mix(v001,v101,fp.x),mix(v011,v111,fp.x),fp.y),fp.z);}
`;const blobMaterial = new THREE.ShaderMaterial({uniforms: {uTime: { value: 0 },ditherScale: { value: 1.5 },colorLevels: { value: 2.0 },uDitherMotionSpeed: { value: DITHER_MOTION_SPEED },uDitherMotionAmplitude: { value: DITHER_MOTION_AMPLITUDE },uBaseColor: { value: new THREE.Color(0xffffff) },uFresnelPower: { value: 2.5 },uVertexNoiseFrequency: { value: BLOB_NOISE_FREQUENCY_VERTEX },uVertexNoiseAmplitude: { value: BLOB_NOISE_AMPLITUDE_VERTEX },uVertexNoiseSpeed: { value: BLOB_NOISE_SPEED_VERTEX },uSurfaceNoiseFrequency: { value: 2.8 },uSurfaceNoiseAmplitude: { value: 0.22 },uDitherPattern: { value: 0 },uCoreBrightness: { value: 0.2 }},vertexShader: `uniform float uTime;uniform float uVertexNoiseFrequency;uniform float uVertexNoiseAmplitude;uniform float uVertexNoiseSpeed;varying vec3 vNormal;varying vec3 vViewPosition;varying vec3 vWorldPosition;${glslRandFunction}void main() {vec3 pos = position;float displacement = snoise(pos * uVertexNoiseFrequency + uTime * uVertexNoiseSpeed) * uVertexNoiseAmplitude;displacement += snoise(pos * uVertexNoiseFrequency * 2.2 + uTime * uVertexNoiseSpeed * 1.4) * (uVertexNoiseAmplitude * 0.45);pos += normal * displacement;vec3 offset = vec3(0.01, 0.01, 0.01);float ddx_noise_orig = snoise((position + offset.xyy) * uVertexNoiseFrequency + uTime * uVertexNoiseSpeed) * uVertexNoiseAmplitude;ddx_noise_orig += snoise((position + offset.xyy) * uVertexNoiseFrequency * 2.2 + uTime * uVertexNoiseSpeed * 1.4) * (uVertexNoiseAmplitude * 0.45);vec3 p_ddx = (position + offset.xyy) + normal * ddx_noise_orig;float ddy_noise_orig = snoise((position + offset.yxy) * uVertexNoiseFrequency + uTime * uVertexNoiseSpeed) * uVertexNoiseAmplitude;ddy_noise_orig += snoise((position + offset.yxy) * uVertexNoiseFrequency * 2.2 + uTime * uVertexNoiseSpeed * 1.4) * (uVertexNoiseAmplitude * 0.45);vec3 p_ddy = (position + offset.yxy) + normal * ddy_noise_orig;vec3 tangent = normalize(p_ddx - pos);vec3 bitangent = normalize(p_ddy - pos);vec3 displacedNormal = normalize(cross(tangent, bitangent));if (length(displacedNormal) < 0.1) { displacedNormal = normal; }vNormal = normalize(normalMatrix * displacedNormal);vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);vViewPosition = -mvPosition.xyz;vWorldPosition = (modelMatrix * vec4(pos, 1.0)).xyz;gl_Position = projectionMatrix * mvPosition;}`,fragmentShader: `uniform float uTime;uniform float ditherScale;uniform float colorLevels;uniform float uDitherMotionSpeed;uniform float uDitherMotionAmplitude;uniform vec3 uBaseColor;uniform float uFresnelPower;uniform float uSurfaceNoiseFrequency;uniform float uSurfaceNoiseAmplitude;uniform int uDitherPattern;uniform float uCoreBrightness;varying vec3 vNormal;varying vec3 vViewPosition;varying vec3 vWorldPosition;${ditherPatternsFunction}${glslRandFunction}void main() {vec3 normal = normalize(vNormal);vec3 viewDir = normalize(vViewPosition);float fresnel = pow(1.0 - abs(dot(viewDir, normal)), uFresnelPower);fresnel = smoothstep(0.0, 1.0, fresnel) * 0.6 + 0.4;float rim = pow(1.0 - abs(dot(viewDir, normal)), 10.0);fresnel += rim * 0.25;float surfaceNoise1 = snoise(vWorldPosition * uSurfaceNoiseFrequency + vec3(uTime * 0.1, uTime * 0.06, uTime * 0.08));float surfaceNoise2 = snoise(vWorldPosition * uSurfaceNoiseFrequency * 2.7 + vec3(uTime * 0.15, uTime * 0.1, uTime * -0.04)) * 0.45;float surfaceNoise = (surfaceNoise1 + surfaceNoise2) * 0.5 + 0.5;surfaceNoise = surfaceNoise * uSurfaceNoiseAmplitude + (1.0 - uSurfaceNoiseAmplitude * 0.6);float coreGlow = pow(max(0.0, dot(viewDir, normal)), 2.0) * uCoreBrightness;float intensity = (fresnel + coreGlow) * surfaceNoise;intensity = clamp(intensity, 0.02, 1.0);vec3 finalColor = uBaseColor * intensity;vec2 screenPos = gl_FragCoord.xy / ditherScale;vec3 ditheredOutput = ditherMonochrome(finalColor, screenPos, colorLevels, uTime, uDitherMotionSpeed, uDitherMotionAmplitude, uDitherPattern);gl_FragColor = vec4(ditheredOutput, 1.0);}`,});const blobGeometry = new THREE.SphereGeometry(BLOB_BASE_RADIUS, 128, 128);const morphingBlob = new THREE.Mesh(blobGeometry, blobMaterial);scene.add(morphingBlob);const particleGeometry = new THREE.BufferGeometry();const particlePositions = new Float32Array(PARTICLE_COUNT * 3);const particleSizes = new Float32Array(PARTICLE_COUNT);const particleSpeeds = new Float32Array(PARTICLE_COUNT);for (let i = 0; i < PARTICLE_COUNT; i++) {const radius = BLOB_BASE_RADIUS * 2.5 + Math.random() * BLOB_BASE_RADIUS * 4;const theta = Math.random() * Math.PI * 2;const phi = Math.acos(2 * Math.random() - 1);particlePositions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);particlePositions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);particlePositions[i * 3 + 2] = radius * Math.cos(phi);particleSizes[i] = Math.random() * 0.06 + 0.015;particleSpeeds[i] = Math.random() * 0.2 + 0.1;}particleGeometry.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));particleGeometry.setAttribute('size', new THREE.BufferAttribute(particleSizes, 1));particleGeometry.setAttribute('speed', new THREE.BufferAttribute(particleSpeeds, 1));const particleMaterial = new THREE.ShaderMaterial({uniforms: {uTime: { value: 0 },uColor: { value: new THREE.Color(0xddddff) },uBlobBaseRadius: { value: BLOB_BASE_RADIUS }},vertexShader: `uniform float uTime;uniform float uBlobBaseRadius;attribute float size;attribute float speed;varying float vDistance;varying float vParticleAlpha;void main() {vec3 pos = position;float waveX = sin(uTime * (speed * 0.8) + position.y * 0.15) * 0.12;float waveY = cos(uTime * (speed * 1.0) + position.z * 0.20) * 0.12;float waveZ = sin(uTime * (speed * 0.9) + position.x * 0.18) * 0.12;pos += vec3(waveX, waveY, waveZ);vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);vDistance = length(mvPosition.xyz);gl_PointSize = size * (400.0 / -mvPosition.z);vParticleAlpha = smoothstep(uBlobBaseRadius * 6.0, uBlobBaseRadius * 2.0, vDistance);gl_Position = projectionMatrix * mvPosition;}`,fragmentShader: `uniform float uTime;uniform vec3 uColor;varying float vDistance;varying float vParticleAlpha;void main() {float dist = length(gl_PointCoord - vec2(0.5));if (dist > 0.5) discard;float pulse = 0.4 + 0.6 * abs(sin(uTime * (1.0 + mod(vDistance, 1.0) * 0.5) + vDistance * 0.2));float finalAlpha = (1.0 - dist * 2.0) * pulse * vParticleAlpha;finalAlpha = clamp(finalAlpha, 0.0, 0.5);gl_FragColor = vec4(uColor, finalAlpha * 0.4);}`,transparent: true,blending: THREE.AdditiveBlending,depthWrite: false});const particleSystem = new THREE.Points(particleGeometry, particleMaterial);scene.add(particleSystem);const ditherPatternSelect = document.getElementById('dither-pattern');const ditherScaleSelect = document.getElementById('dither-scale');const uiContainer = document.getElementById('ui-container');ditherPatternSelect.addEventListener('change', (e) => {blobMaterial.uniforms.uDitherPattern.value = parseInt(e.target.value);});ditherScaleSelect.addEventListener('change', (e) => {blobMaterial.uniforms.ditherScale.value = parseFloat(e.target.value);});let uiTimeout;const uiAutoHideDelay = 3000;const resetUITimeout = () => {clearTimeout(uiTimeout);uiContainer.classList.remove('opacity-0', 'translate-x-12');uiContainer.classList.add('opacity-100', 'translate-x-0');uiTimeout = setTimeout(() => {uiContainer.classList.remove('opacity-100', 'translate-x-0');uiContainer.classList.add('opacity-0', 'translate-x-12');}, uiAutoHideDelay);};document.addEventListener('mousemove', resetUITimeout);document.addEventListener('click', resetUITimeout);document.addEventListener('touchstart', resetUITimeout);resetUITimeout();window.addEventListener('resize', () => {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth, window.innerHeight);composer.setSize(window.innerWidth, window.innerHeight);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));bloomPass.resolution.set(window.innerWidth, window.innerHeight);});camera.lookAt(scene.position);const clock = new THREE.Clock();function animate() {requestAnimationFrame(animate);const elapsedTime = clock.getElapsedTime();blobMaterial.uniforms.uTime.value = elapsedTime;particleMaterial.uniforms.uTime.value = elapsedTime;starMaterial.uniforms.uTime.value = elapsedTime;morphingBlob.rotation.x += 0.0004;morphingBlob.rotation.y += 0.0007;pointLight1.position.x = Math.sin(elapsedTime * 0.32) * 6;pointLight1.position.z = Math.cos(elapsedTime * 0.32) * 6;pointLight2.position.y = Math.sin(elapsedTime * 0.18) * 4;pointLight2.position.x = Math.cos(elapsedTime * 0.25) * -5;pointLight3.position.z = Math.cos(elapsedTime * 0.40) * 5;pointLight3.position.y = Math.sin(elapsedTime * 0.35) * -4;controls.update();composer.render();}window.onload = () => {animate();};
</script>
</html>
效果如下
