Codrops 원본
Implementing a Dissolve Effect with Shaders and Particles in Three.js
배경 · MIT
갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.
SOURCE FILES
원본 코드 읽기
수집한 원본 소스와 실행 안내를 함께 제공합니다.
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="stylesheet" href="./src/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dissolve-Effect</title>
</head>
<body class="loading">
<header class="fixed top-0 left-0 w-full pointer-events-none z-50">
<div class="absolute top-0 left-0 p-4 pointer-events-auto">
<h1 class="text-white font-bold text-lg">Dissolve Effect</h1>
<nav class="flex flex-wrap gap-4 mt-2 max-sm:w-[150px]">
<a class="text-white font-bold" href="https://tympanus.net/codrops/?p=">Tutorial</a>
<a class="text-white font-bold" href="https://github.com/JatinChopra/emissive-dissolve-effect">GitHub</a>
<a class="text-white font-bold" href="https://tympanus.net/codrops/demos/">All demos</a>
</nav>
</div>
</header>
<canvas id="c" class="z-0"></canvas>
<script type="module" src="/src/main.ts"></script>
<footer class="fixed bottom-0 left-0 w-full pointer-events-none z-50">
<div class="absolute bottom-0 left-0 p-4 pointer-events-auto z-2">
<nav class="flex flex-wrap gap-2">
<a class="text-white" href="https://tympanus.net/codrops/demos/?tag=glsl">#glsl</a>
<a class="text-white" href="https://tympanus.net/codrops/demos/?tag=three-js">#three.js</a>
<a class="text-white" href="https://tympanus.net/codrops/demos/?tag=webgl">#webgl</a>
</nav>
</div>
</footer>
</body>
</html>
src/lib/noise/cnoise.glsl
// Classic Perlin 3D Noise
// by Stefan Gustavson (https://github.com/stegu/webgl-noise)
//
vec4 permute(vec4 x) {
return mod(((x * 34.0) + 1.0) * x, 289.0);
}
vec4 taylorInvSqrt(vec4 r) {
return 1.79284291400159 - 0.85373472095314 * r;
}
vec3 fade(vec3 t) {
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
float cnoise(vec3 P) {
vec3 Pi0 = floor(P); // Integer part for indexing
vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
Pi0 = mod(Pi0, 289.0);
Pi1 = mod(Pi1, 289.0);
vec3 Pf0 = fract(P); // Fractional part for interpolation
vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
vec4 iy = vec4(Pi0.yy, Pi1.yy);
vec4 iz0 = Pi0.zzzz;
vec4 iz1 = Pi1.zzzz;
vec4 ixy = permute(permute(ix) + iy);
vec4 ixy0 = permute(ixy + iz0);
vec4 ixy1 = permute(ixy + iz1);
vec4 gx0 = ixy0 / 7.0;
vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
gx0 = fract(gx0);
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
vec4 sz0 = step(gz0, vec4(0.0));
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
vec4 gx1 = ixy1 / 7.0;
vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
gx1 = fract(gx1);
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
vec4 sz1 = step(gz1, vec4(0.0));
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
vec3 g000 = vec3(gx0.x, gy0.x, gz0.x);
vec3 g100 = vec3(gx0.y, gy0.y, gz0.y);
vec3 g010 = vec3(gx0.z, gy0.z, gz0.z);
vec3 g110 = vec3(gx0.w, gy0.w, gz0.w);
vec3 g001 = vec3(gx1.x, gy1.x, gz1.x);
vec3 g101 = vec3(gx1.y, gy1.y, gz1.y);
vec3 g011 = vec3(gx1.z, gy1.z, gz1.z);
vec3 g111 = vec3(gx1.w, gy1.w, gz1.w);
vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
g000 *= norm0.x;
g010 *= norm0.y;
g100 *= norm0.z;
g110 *= norm0.w;
vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
g001 *= norm1.x;
g011 *= norm1.y;
g101 *= norm1.z;
g111 *= norm1.w;
float n000 = dot(g000, Pf0);
float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
float n111 = dot(g111, Pf1);
vec3 fade_xyz = fade(Pf0);
vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
return 2.2 * n_xyz;
}
함께 쓰는 파일 8개 보기
src/lib/noise/snoise.glsl
vec4 permute(vec4 x) {
return mod(((x * 34.0) + 1.0) * x, 289.0);
}
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);
// First corner
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
// Other corners
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);
// x0 = x0 - 0. + 0.0 * C
vec3 x1 = x0 - i1 + 1.0 * C.xxx;
vec3 x2 = x0 - i2 + 2.0 * C.xxx;
vec3 x3 = x0 - 1. + 3.0 * C.xxx;
// Permutations
i = mod(i, 289.0);
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));
// Gradients
// ( N*N points uniformly over a square, mapped onto an octahedron.)
float n_ = 1.0 / 7.0; // N=7
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,N*N)
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_); // mod(j,N)
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);
//Normalise gradients
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;
// Mix final noise value
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)));
}
src/main.ts
import './style.css'
import * as THREE from 'three';
//import Stats from 'three/examples/jsm/libs/stats.module.js';
import { Pane } from 'tweakpane';
//import { LightProbeGenerator } from 'three/examples/jsm/Addons.js';
import { OrbitControls } from 'three/examples/jsm/Addons.js';
import snoise from './lib/noise/snoise.glsl?raw';
import { EffectComposer, RenderPass, OutputPass, UnrealBloomPass, ShaderPass } from 'three/examples/jsm/Addons.js';
import { TeapotGeometry } from 'three/examples/jsm/Addons.js';
import { BladeApi } from 'tweakpane';
let scale = 1.0;
function isMobileDevice() {
return /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
}
if (isMobileDevice()) scale = 0.7;
const cnvs = document.getElementById('c') as HTMLCanvasElement;
const scene = new THREE.Scene();
const cam = new THREE.PerspectiveCamera(75, cnvs.clientWidth / cnvs.clientHeight, 0.001, 100);
if (isMobileDevice()) cam.position.set(0, 8, 18)
else cam.position.set(0, 1, 14);
const blackColor = new THREE.Color(0x000000);
scene.background = blackColor;
const re = new THREE.WebGLRenderer({ canvas: cnvs, antialias: true });
re.setPixelRatio(window.devicePixelRatio);
re.setSize(cnvs.clientWidth * scale, cnvs.clientHeight * scale, false);
re.toneMapping = THREE.CineonToneMapping;
re.outputColorSpace = THREE.SRGBColorSpace;
const effectComposer1 = new EffectComposer(re);
const renderPass = new RenderPass(scene, cam);
let radius = isMobileDevice() ? 0.1 : 0.25;
const unrealBloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerHeight * scale, window.innerWidth * scale), 0.5, radius, 0.2);
const outPass = new OutputPass();
const effectComposer2 = new EffectComposer(re);
const shaderPass = new ShaderPass(new THREE.ShaderMaterial({
uniforms: {
tDiffuse: { value: null },
uBloomTexture: {
value: effectComposer1.renderTarget2.texture
},
uStrength: {
value: isMobileDevice() ? 6.00 : 8.00,
},
},
vertexShader: `
varying vec2 vUv;
void main(){
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform sampler2D uBloomTexture;
uniform float uStrength;
varying vec2 vUv;
void main(){
vec4 baseEffect = texture2D(tDiffuse,vUv);
vec4 bloomEffect = texture2D(uBloomTexture,vUv);
gl_FragColor =baseEffect + bloomEffect * uStrength;
}
`,
}));
effectComposer1.addPass(renderPass);
effectComposer1.addPass(unrealBloomPass);
effectComposer1.renderToScreen = false;
effectComposer2.addPass(renderPass);
effectComposer2.addPass(shaderPass);
effectComposer2.addPass(outPass);
//const stat = new Stats();
const orbCtrls = new OrbitControls(cam, cnvs);
//document.body.appendChild(stat.dom);
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256);
const cubeCamera = new THREE.CubeCamera(0.1, 500, cubeRenderTarget);
//let lightProbe = new THREE.LightProbe();
let cubeTextureUrls: string[];
let cubeTexture: THREE.CubeTexture;
function generateCubeUrls(prefix: string, postfix: string) {
return [
prefix + 'posx' + postfix, prefix + 'negx' + postfix,
prefix + 'posy' + postfix, prefix + 'negy' + postfix,
prefix + 'posz' + postfix, prefix + 'negz' + postfix
];
}
cubeTextureUrls = generateCubeUrls('/cubeMap2/', '.png');
async function loadTextures() {
const cubeTextureLoader = new THREE.CubeTextureLoader();
cubeTexture = await cubeTextureLoader.loadAsync(cubeTextureUrls);
scene.background = cubeTexture;
scene.environment = cubeTexture;
cubeCamera.update(re, scene);
document.body.classList.remove("loading");
//lightProbe = await LightProbeGenerator.fromCubeRenderTarget(re, cubeRenderTarget);
//scene.add(lightProbe);
}
loadTextures();
let segments1 = isMobileDevice() ? 90 : 140;
let segments2 = isMobileDevice() ? 18 : 32;
const sphere = new THREE.SphereGeometry(4.5, segments1, segments1);
const teaPot = new TeapotGeometry(3, segments2);
const torus = new THREE.TorusGeometry(3, 1.5, segments1, segments1);
const torusKnot = new THREE.TorusKnotGeometry(2.5, 0.8, segments1, segments1);
let geoNames = ["TorusKnot", "Tea Pot", "Sphere", "Torus"];
let geometries = [torusKnot, teaPot, sphere, torus];
let particleTexture: THREE.Texture;
particleTexture = new THREE.TextureLoader().load('/particle.png')
let mesh: THREE.Object3D;
let meshGeo: THREE.BufferGeometry;
meshGeo = geometries[0];
const phyMat = new THREE.MeshPhysicalMaterial();
phyMat.color = new THREE.Color(0x636363);
phyMat.metalness = 2.0;
phyMat.roughness = 0.0;
phyMat.side = THREE.DoubleSide;
const dissolveUniformData = {
uEdgeColor: {
value: new THREE.Color(0x4d9bff),
},
uFreq: {
value: 0.25,
},
uAmp: {
value: 16.0
},
uProgress: {
value: -7.0
},
uEdge: {
value: 0.8
}
}
function setupUniforms(shader: THREE.WebGLProgramParametersWithUniforms, uniforms: { [uniform: string]: THREE.IUniform<any> }) {
const keys = Object.keys(uniforms);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
shader.uniforms[key] = uniforms[key];
}
}
function setupDissolveShader(shader: THREE.WebGLProgramParametersWithUniforms) {
// vertex shader snippet outside main
shader.vertexShader = shader.vertexShader.replace('#include <common>', `#include <common>
varying vec3 vPos;
`);
// vertex shader snippet inside main
shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>', `#include <begin_vertex>
vPos = position;
`);
// fragment shader snippet outside main
shader.fragmentShader = shader.fragmentShader.replace('#include <common>', `#include <common>
varying vec3 vPos;
uniform float uFreq;
uniform float uAmp;
uniform float uProgress;
uniform float uEdge;
uniform vec3 uEdgeColor;
${snoise}
`);
// fragment shader snippet inside main
shader.fragmentShader = shader.fragmentShader.replace('#include <dithering_fragment>', `#include <dithering_fragment>
float noise = snoise(vPos * uFreq) * uAmp; // calculate snoise in fragment shader for smooth dissolve edges
if(noise < uProgress) discard; // discard any fragment where noise is lower than progress
float edgeWidth = uProgress + uEdge;
if(noise > uProgress && noise < edgeWidth){
gl_FragColor = vec4(vec3(uEdgeColor),noise); // colors the edge
}else{
gl_FragColor = vec4(gl_FragColor.xyz,1.0);
}
`);
}
phyMat.onBeforeCompile = (shader) => {
setupUniforms(shader, dissolveUniformData);
setupDissolveShader(shader);
}
mesh = new THREE.Mesh(meshGeo, phyMat);
scene.add(mesh);
let particleMesh: THREE.Points;
let particleMat = new THREE.ShaderMaterial();
particleMat.transparent = true;
particleMat.blending = THREE.AdditiveBlending;
let particleCount = meshGeo.attributes.position.count;
let particleMaxOffsetArr: Float32Array; // -- how far a particle can go from its initial position
let particleInitPosArr: Float32Array; // store the initial position of the particles -- particle position will reset here if it exceed maxoffset
let particleCurrPosArr: Float32Array; // use to update he position of the particle
let particleVelocityArr: Float32Array; // velocity of each particle
let particleDistArr: Float32Array;
let particleRotationArr: Float32Array;
let particleData = {
particleSpeedFactor: 0.02, // for tweaking velocity
velocityFactor: { x: 2.5, y: 2 },
waveAmplitude: 0,
}
function initParticleAttributes(meshGeo: THREE.BufferGeometry) {
particleCount = meshGeo.attributes.position.count;
particleMaxOffsetArr = new Float32Array(particleCount);
particleInitPosArr = new Float32Array(meshGeo.getAttribute('position').array);
particleCurrPosArr = new Float32Array(meshGeo.getAttribute('position').array);
particleVelocityArr = new Float32Array(particleCount * 3);
particleDistArr = new Float32Array(particleCount);
particleRotationArr = new Float32Array(particleCount);
for (let i = 0; i < particleCount; i++) {
let x = i * 3 + 0;
let y = i * 3 + 1;
let z = i * 3 + 2;
particleMaxOffsetArr[i] = Math.random() * 5.5 + 1.5;
particleVelocityArr[x] = Math.random() * 0.5 + 0.5;
particleVelocityArr[y] = Math.random() * 0.5 + 0.5;
particleVelocityArr[z] = Math.random() * 0.1;
particleDistArr[i] = 0.001;
particleRotationArr[i] = Math.random() * Math.PI * 2;
}
meshGeo.setAttribute('aOffset', new THREE.BufferAttribute(particleMaxOffsetArr, 1));
meshGeo.setAttribute('aCurrentPos', new THREE.BufferAttribute(particleCurrPosArr, 3));
meshGeo.setAttribute('aVelocity', new THREE.BufferAttribute(particleVelocityArr, 3));
meshGeo.setAttribute('aDist', new THREE.BufferAttribute(particleDistArr, 1));
meshGeo.setAttribute('aAngle', new THREE.BufferAttribute(particleRotationArr, 1));
}
function calculateWaveOffset(idx: number) {
const posx = particleCurrPosArr[idx * 3 + 0];
const posy = particleCurrPosArr[idx * 3 + 1];
let xwave1 = Math.sin(posy * 2) * (0.8 + particleData.waveAmplitude);
let ywave1 = Math.sin(posx * 2) * (0.6 + particleData.waveAmplitude);
let xwave2 = Math.sin(posy * 5) * (0.2 + particleData.waveAmplitude);
let ywave2 = Math.sin(posx * 1) * (0.9 + particleData.waveAmplitude);
let xwave3 = Math.sin(posy * 8) * (0.8 + particleData.waveAmplitude);
let ywave3 = Math.sin(posx * 5) * (0.6 + particleData.waveAmplitude);
let xwave4 = Math.sin(posy * 3) * (0.8 + particleData.waveAmplitude);
let ywave4 = Math.sin(posx * 7) * (0.6 + particleData.waveAmplitude);
let xwave = xwave1 + xwave2 + xwave3 + xwave4;
let ywave = ywave1 + ywave2 + ywave3 + ywave4;
return { xwave, ywave }
}
function updateVelocity(idx: number) {
let vx = particleVelocityArr[idx * 3 + 0];
let vy = particleVelocityArr[idx * 3 + 1];
let vz = particleVelocityArr[idx * 3 + 2];
vx *= particleData.velocityFactor.x;
vy *= particleData.velocityFactor.y;
let { xwave, ywave } = calculateWaveOffset(idx);
vx += xwave;
vy += ywave;
vx *= Math.abs(particleData.particleSpeedFactor);
vy *= Math.abs(particleData.particleSpeedFactor);
vz *= Math.abs(particleData.particleSpeedFactor);
return { vx, vy, vz }
}
function updateParticleAttriutes() {
for (let i = 0; i < particleCount; i++) {
let x = i * 3 + 0;
let y = i * 3 + 1;
let z = i * 3 + 2;
let { vx, vy, vz } = updateVelocity(i);
particleCurrPosArr[x] += vx;
particleCurrPosArr[y] += vy;
particleCurrPosArr[z] += vz;
const vec1 = new THREE.Vector3(particleInitPosArr[x], particleInitPosArr[y], particleInitPosArr[z]);
const vec2 = new THREE.Vector3(particleCurrPosArr[x], particleCurrPosArr[y], particleCurrPosArr[z]);
const dist = vec1.distanceTo(vec2);
particleDistArr[i] = dist;
particleRotationArr[i] += 0.01;
if (dist > particleMaxOffsetArr[i]) {
particleCurrPosArr[x] = particleInitPosArr[x];
particleCurrPosArr[y] = particleInitPosArr[y];
particleCurrPosArr[z] = particleInitPosArr[z];
}
}
meshGeo.setAttribute('aOffset', new THREE.BufferAttribute(particleMaxOffsetArr, 1));
meshGeo.setAttribute('aCurrentPos', new THREE.BufferAttribute(particleCurrPosArr, 3));
meshGeo.setAttribute('aVelocity', new THREE.BufferAttribute(particleVelocityArr, 3));
meshGeo.setAttribute('aDist', new THREE.BufferAttribute(particleDistArr, 1));
meshGeo.setAttribute('aAngle', new THREE.BufferAttribute(particleRotationArr, 1));
}
initParticleAttributes(meshGeo);
const particlesUniformData = {
uTexture: {
value: particleTexture,
},
uPixelDensity: {
value: re.getPixelRatio()
},
uProgress: dissolveUniformData.uProgress,
uEdge: dissolveUniformData.uEdge,
uAmp: dissolveUniformData.uAmp,
uFreq: dissolveUniformData.uFreq,
uBaseSize: {
value: isMobileDevice() ? 40 : 80,
},
uColor: {
value: new THREE.Color(0x4d9bff),
}
}
particleMat.uniforms = particlesUniformData;
particleMat.vertexShader = `
${snoise}
uniform float uPixelDensity;
uniform float uBaseSize;
uniform float uFreq;
uniform float uAmp;
uniform float uEdge;
uniform float uProgress;
varying float vNoise;
varying float vAngle;
attribute vec3 aCurrentPos;
attribute float aDist;
attribute float aAngle;
void main() {
vec3 pos = position;
float noise = snoise(pos * uFreq) * uAmp;
vNoise =noise;
vAngle = aAngle;
if( vNoise > uProgress-2.0 && vNoise < uProgress + uEdge+2.0){
pos = aCurrentPos;
}
vec4 modelPosition = modelMatrix * vec4(pos, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
float size = uBaseSize * uPixelDensity;
size = size / (aDist + 1.0);
gl_PointSize = size / -viewPosition.z;
}
`;
particleMat.fragmentShader = `
uniform vec3 uColor;
uniform float uEdge;
uniform float uProgress;
uniform sampler2D uTexture;
varying float vNoise;
varying float vAngle;
void main(){
if( vNoise < uProgress ) discard;
if( vNoise > uProgress + uEdge) discard;
vec2 coord = gl_PointCoord;
coord = coord - 0.5; // get the coordinate from 0-1 ot -0.5 to 0.5
coord = coord * mat2(cos(vAngle),sin(vAngle) , -sin(vAngle), cos(vAngle)); // apply the rotation transformaion
coord = coord + 0.5; // reset the coordinate to 0-1
vec4 texture = texture2D(uTexture,coord);
gl_FragColor = vec4(vec3(uColor.xyz * texture.xyz),1.0);
}
`;
particleMesh = new THREE.Points(meshGeo, particleMat);
scene.add(particleMesh);
function resizeRendererToDisplaySize() {
const width = cnvs.clientWidth * scale;
const height = cnvs.clientHeight * scale;
const needResize = cnvs.width !== width || cnvs.height !== height;
if (needResize) {
re.setSize(width, height, false);
renderPass.setSize(width, height);
outPass.setSize(width, height);
unrealBloomPass.setSize(width, height);
effectComposer1.setSize(width, height);
effectComposer2.setSize(width, height);
}
return needResize;
}
let tweaks = {
x: 0,
z: 0,
dissolveProgress: dissolveUniformData.uProgress.value,
edgeWidth: dissolveUniformData.uEdge.value,
amplitude: dissolveUniformData.uAmp.value,
frequency: dissolveUniformData.uFreq.value,
meshVisible: true,
meshColor: "#" + phyMat.color.getHexString(),
edgeColor: "#" + dissolveUniformData.uEdgeColor.value.getHexString(),
autoDissolve: false,
particleVisible: true,
particleBaseSize: particlesUniformData.uBaseSize.value,
particleColor: "#" + particlesUniformData.uColor.value.getHexString(),
particleSpeedFactor: particleData.particleSpeedFactor,
velocityFactor: particleData.velocityFactor,
waveAmplitude: particleData.waveAmplitude,
bloomStrength: shaderPass.uniforms.uStrength.value,
rotationY: mesh.rotation.y,
};
function createTweakList(name: string, keys: string[], vals: any[]): BladeApi {
const opts = [];
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const v = vals[i];
opts.push({ text: k, value: v });
}
return pane.addBlade({
view: 'list', label: name,
options: opts,
value: vals[0]
})
}
function handleMeshChange(geo: any) {
scene.remove(mesh);
scene.remove(particleMesh);
meshGeo = geo;
mesh = new THREE.Mesh(geo, phyMat);
initParticleAttributes(geo);
particleMesh = new THREE.Points(geo, particleMat);
scene.add(mesh);
scene.add(particleMesh);
}
const pane = new Pane();
const controller = pane.addFolder({ title: "Controls", expanded: false });
const meshFolder = controller.addFolder({ title: "Mesh", expanded: false });
let meshBlade = createTweakList('Mesh', geoNames, geometries);
//@ts-ignore
meshBlade.on('change', (val) => { handleMeshChange(val.value) })
meshFolder.add(meshBlade);
meshFolder.addBinding(tweaks, "bloomStrength", { min: 1, max: 20, step: 0.01, label: "Bloom Strength" }).on('change', (obj) => { shaderPass.uniforms.uStrength.value = obj.value; })
meshFolder.addBinding(tweaks, "rotationY", { min: -(Math.PI * 2), max: (Math.PI * 2), step: 0.01, label: "Rotation Y" }).on('change', (obj) => { particleMesh.rotation.y = mesh.rotation.y = obj.value; });
const dissolveFolder = controller.addFolder({ title: "Dissolve Effect", expanded: false, });
dissolveFolder.addBinding(tweaks, "meshVisible", { label: "Visible" }).on('change', (obj) => { mesh.visible = obj.value; });
let progressBinding = dissolveFolder.addBinding(tweaks, "dissolveProgress", { min: -20, max: 20, step: 0.0001, label: "Progress" }).on('change', (obj) => { dissolveUniformData.uProgress.value = obj.value; });
dissolveFolder.addBinding(tweaks, "autoDissolve", { label: "Auto Animate" }).on('change', (obj) => { tweaks.autoDissolve = obj.value });
dissolveFolder.addBinding(tweaks, "edgeWidth", { min: 0.1, max: 8, step: 0.001, label: "Edge Width" }).on('change', (obj) => { dissolveUniformData.uEdge.value = obj.value });
dissolveFolder.addBinding(tweaks, "frequency", { min: 0.001, max: 2, step: 0.001, label: "Frequency" }).on('change', (obj) => { dissolveUniformData.uFreq.value = obj.value });
dissolveFolder.addBinding(tweaks, "amplitude", { min: 0.1, max: 20, step: 0.001, label: "Amplitude" }).on('change', (obj) => { dissolveUniformData.uAmp.value = obj.value });
dissolveFolder.addBinding(tweaks, "meshColor", { label: "Mesh Color" }).on('change', (obj) => { phyMat.color.set(obj.value) });
dissolveFolder.addBinding(tweaks, "edgeColor", { label: "Edge Color" }).on('change', (obj) => { dissolveUniformData.uEdgeColor.value.set(obj.value); });
const particleFolder = controller.addFolder({ title: "Particle", expanded: false });
particleFolder.addBinding(tweaks, "particleVisible", { label: "Visible" }).on('change', (obj) => { particleMesh.visible = obj.value; });
particleFolder.addBinding(tweaks, "particleBaseSize", { min: 10.0, max: 100, step: 0.01, label: "Base size" }).on('change', (obj) => { particlesUniformData.uBaseSize.value = obj.value; });
particleFolder.addBinding(tweaks, "particleColor", { label: "Color" }).on('change', (obj) => { particlesUniformData.uColor.value.set(obj.value); });
particleFolder.addBinding(tweaks, "particleSpeedFactor", { min: 0.001, max: 0.1, step: 0.001, label: "Speed" }).on('change', (obj) => { particleData.particleSpeedFactor = obj.value });
particleFolder.addBinding(tweaks, "waveAmplitude", { min: 0, max: 5, step: 0.01, label: "Wave Amp" }).on('change', (obj) => { particleData.waveAmplitude = obj.value; });
particleFolder.addBinding(tweaks, "velocityFactor", { expanded: true, picker: 'inline', label: "Velocity Factor" }).on('change', (obj) => { particleData.velocityFactor = obj.value });
let dissolving = true;
let geoIdx = 0;
let geoLength = geometries.length;
function animateDissolve() {
if (!tweaks.autoDissolve) return;
let progress = dissolveUniformData.uProgress;
if (dissolving) {
progress.value += isMobileDevice() ? 0.12 : 0.08;
} else {
progress.value -= isMobileDevice() ? 0.12 : 0.08;
}
if (progress.value > 14 && dissolving) {
dissolving = false;
geoIdx++;
handleMeshChange(geometries[geoIdx % geoLength]);
//@ts-ignore
meshBlade.value = geometries[geoIdx % geoLength];
};
if (progress.value < -17 && !dissolving) dissolving = true;
progressBinding.controller.value.setRawValue(progress.value);
}
function floatMeshes(time: number) {
mesh.position.set(0, Math.sin(time * 2.0) * 0.5, 0);
particleMesh.position.set(0, Math.sin(time * 2.0) * 0.5, 0);
}
const clock = new THREE.Clock();
function animate() {
// stat.update();
orbCtrls.update();
let time = clock.getElapsedTime();
updateParticleAttriutes();
floatMeshes(time);
animateDissolve();
if (resizeRendererToDisplaySize()) {
const canvas = re.domElement;
cam.aspect = canvas.clientWidth / canvas.clientHeight;
cam.updateProjectionMatrix();
}
scene.background = blackColor;
effectComposer1.render();
scene.background = cubeTexture;
effectComposer2.render();
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
window.addEventListener('orientationchange', () => {
location.reload();
});
src/style.css
@import "tailwindcss";
:root {
font-size: 12px;
--color-text: #fff;
--color-bg: #000;
--color-link: #fff;
--color-link-hover: #fff;
--page-padding: 1.5rem;
}
*,
*::after,
*::before {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
height: 100%;
color: var(--color-text);
background-color: var(--color-bg);
font-family: ui-monospace, monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Canvas */
#c {
width: 100vw;
height: 100vh;
display: block;
}
/* Loader */
.loading::before,
.loading::after {
content: '';
position: fixed;
z-index: 10000;
}
.loading::before {
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--color-bg);
}
.loading::after {
top: 50%;
left: 50%;
width: 100px;
height: 1px;
margin: 0 0 0 -50px;
background: var(--color-link);
animation: loaderAnim 1.5s ease-in-out infinite alternate forwards;
}
@keyframes loaderAnim {
0% {
transform: scaleX(0);
transform-origin: 0% 50%;
}
50% {
transform: scaleX(1);
transform-origin: 0% 50%;
}
50.1% {
transform: scaleX(1);
transform-origin: 100% 50%;
}
100% {
transform: scaleX(0);
transform-origin: 100% 50%;
}
}
vite.config.ts
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
tailwindcss(),
],
})
Original author attribution실행 안내·자료
Implementing a Dissolve Effect with Shaders and Particles in Three.js
Original author: Jatin Chopra
Published by Codrops. Copyright (c) 2026 Codrops.
License: MIT, pursuant to the publisher's downloadable-demo grant.
Keep CODROPS-MIT-2026.txt and the original project notices with copies.
Third-party media exceptions and credits are recorded separately in the source inventory.
Media credits and license evidence실행 안내·자료
README.md
## Credits
- Environment maps [Poly Haven](https://polyhaven.com/)
- Noise algorithm [Noise Algorithms](https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83)
## License
[MIT](LICENSE)
Bundled dependency licenses실행 안내·자료
three@0.172.0 — LICENSE
The MIT License
Copyright © 2010-2024 three.js authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
BEHIND THE EXAMPLE
이 장면을 만드는 원리
입자 배열 갱신과 매 프레임 속성 재생성
화면 크기·입력·동시 작업을 고정하고 실제 렌더링 실패와 비용을 관찰합니다.
- 이 예제에서는
- updateParticleAttriutes는 모든 입자의 위치·거리를 CPU에서 갱신하고 최대 이동을 넘으면 초기 위치로 돌립니다. 매 호출에서 다섯 BufferAttribute를 새로 만들어 geometry에 다시 설정합니다.
