slime-simulation
Version:
Interactive WebGL slime simulation with PBR rendering
2 lines (1 loc) • 19.2 kB
JavaScript
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("three")):"function"==typeof define&&define.amd?define(["exports","three"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).SlimeSimulationLib={},e.THREE)}(this,(function(e,t){"use strict";function i(e){var t=Object.create(null);return e&&Object.keys(e).forEach((function(i){if("default"!==i){var n=Object.getOwnPropertyDescriptor(e,i);Object.defineProperty(t,i,n.get?n:{enumerable:!0,get:function(){return e[i]}})}})),t.default=e,Object.freeze(t)}var n=i(t);const a={default:e=>{e.updateSettings({uNoiseFactor:.004,uBirthRate:.92,uDeathRate:.6,uSustainRate:.97,uSpeed:.12,uSampleRadius:4,uGrowthTarget:2.5,uMouseMass:.7,uMouseRadius:.05,uGaussianWeight:8,uBaseColor:new n.Vector3(.1,.01,.8),uSecondaryColor:new n.Vector4(.4,0,.8,.3),uSpecularColor:new n.Vector3(.9,.9,1),uRoughness:.2,uMetalness:.6,uToneMappingDenominator:2.2,uLightPosZ:2,uHeightMultiplier:1,uNormalMultiplier:2,uNormalZComponent:1,uGlassEffect:!1,uShowImage:!0,uSmoothNormals:!0,uSpotlightDampening:!0}),e.resizeSimulation(256)},purple:e=>{e.updateSettings({uNoiseFactor:0,uBirthRate:.99,uDeathRate:.6,uSustainRate:.9,uSpeed:.15,uSampleRadius:3,uGrowthTarget:2,uBaseColor:new n.Vector3(.133,0,.706),uSecondaryColor:new n.Vector4(.547,.106,.149,.5),uSpecularColor:new n.Vector3(.725,.725,.725),uRoughness:.2,uMetalness:.3,uToneMappingDenominator:2.4})},mercury:e=>{e.updateSettings({uNoiseFactor:.006,uBirthRate:.88,uDeathRate:.65,uSustainRate:.94,uSpeed:.18,uSampleRadius:3.5,uGrowthTarget:1.8,uMouseMass:.9,uMouseRadius:.04,uGaussianWeight:6,uBaseColor:new n.Vector3(.8,.8,.85),uSecondaryColor:new n.Vector4(.9,.9,1,.2),uSpecularColor:new n.Vector3(1,1,1),uRoughness:.05,uMetalness:1,uToneMappingDenominator:2.3,uLightPosZ:2.2,uHeightMultiplier:.9,uNormalMultiplier:3,uNormalZComponent:.8,uGlassEffect:!1,uShowImage:!0,uSmoothNormals:!0,uSpotlightDampening:!0}),e.resizeSimulation(384)},emerald:e=>{e.updateSettings({uNoiseFactor:.004,uBirthRate:.9,uDeathRate:.9,uSustainRate:.98,uSpeed:.1,uSampleRadius:4.5,uGrowthTarget:2.2,uMouseMass:.7,uMouseRadius:.05,uGaussianWeight:9,uBaseColor:new n.Vector3(0,.1,.01),uSecondaryColor:new n.Vector4(0,.2,.01,.5),uSpecularColor:new n.Vector3(.8,1,.9),uRoughness:.1,uMetalness:.5,uToneMappingDenominator:2.5,uLightPosZ:2,uHeightMultiplier:1.1,uNormalMultiplier:2.2,uNormalZComponent:1,uGlassEffect:!1,uShowImage:!0,uSmoothNormals:!0,uSpotlightDampening:!0}),e.resizeSimulation(256)},glass:e=>{e.updateSettings({uNoiseFactor:.005,uBirthRate:.85,uDeathRate:.6,uSustainRate:.95,uSpeed:.15,uSampleRadius:4,uGrowthTarget:1.5,uMouseMass:.6,uMouseRadius:.05,uGaussianWeight:8,uBaseColor:new n.Vector3(.98,.98,.98),uSecondaryColor:new n.Vector4(1,1,1,.05),uSpecularColor:new n.Vector3(1,1,1),uRoughness:.05,uMetalness:.1,uToneMappingDenominator:2.2,uLightPosZ:2,uHeightMultiplier:1,uNormalMultiplier:2,uNormalZComponent:1,uGlassEffect:!0,uShowImage:!0,uSmoothNormals:!0,uSpotlightDampening:!0}),e.resizeSimulation(384)},raw:e=>{e.updateSettings({uNoiseFactor:.005,uBirthRate:.9,uDeathRate:.6,uSustainRate:.985,uSpeed:.1,uSampleRadius:4,uGrowthTarget:10,uMouseMass:.6,uMouseRadius:.05,uGaussianWeight:8,uBaseColor:new n.Vector3(0,0,0),uSecondaryColor:new n.Vector4(0,.01,.02,1),uSpecularColor:new n.Vector3(.6,.6,.6),uRoughness:.15,uMetalness:.9,uToneMappingDenominator:2.2,uLightPosZ:1.5,uHeightMultiplier:1.5,uNormalMultiplier:4,uNormalZComponent:.8,uGlassEffect:!1,uShowImage:!0,uSmoothNormals:!1,uSpotlightDampening:!0}),e.resizeSimulation(128)}},o="\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}";e.SlimeSimulation=class{constructor(e,t={}){this.container=e,this.options={baseResolution:128,stretchFactor:1,showGui:!1,imagePath:null,preset:"default",params:{},...t},this.canvas=document.createElement("canvas"),this.canvas.style.position="absolute",this.canvas.style.top="0",this.canvas.style.left="0",this.canvas.style.width="100%",this.canvas.style.height="100%",this.canvas.style.zIndex="-1",this.container.appendChild(this.canvas),this.mouseTimeout=null,this.isTouch=!1,this.touchActive=!1,this.options.imagePath?this.loadTextures().then((()=>{this.init()})):(this.imageTexture=new n.Texture,this.init())}async loadTextures(){const e=new n.TextureLoader;try{this.options.imagePath?this.imageTexture=await(t=this.options.imagePath,new Promise(((i,n)=>{e.load(t,i,void 0,n)}))):this.imageTexture=new n.Texture,this.imageTexture.wrapS=n.RepeatWrapping,this.imageTexture.wrapT=n.RepeatWrapping}catch(e){console.error("Error loading texture:",e),this.imageTexture=new n.Texture}var t}initRenderer(){this.renderer=new n.WebGLRenderer({canvas:this.canvas,antialias:!0}),this.renderer.setSize(window.innerWidth,window.innerHeight),this.renderer.setPixelRatio(window.devicePixelRatio)}initScene(){this.scene=new n.Scene,this.camera=new n.OrthographicCamera(-1,1,1,-1,0,1),this.quad=new n.PlaneGeometry(2,2)}initSimulation(){const e=window.innerWidth/window.innerHeight;this.sizes={width:Math.round(this.options.baseResolution*e*this.options.stretchFactor),height:this.options.baseResolution},this.renderTargets=[new n.WebGLRenderTarget(this.sizes.width,this.sizes.height,{minFilter:n.LinearFilter,magFilter:n.LinearFilter,format:n.RGBAFormat,type:n.HalfFloatType}),new n.WebGLRenderTarget(this.sizes.width,this.sizes.height,{minFilter:n.LinearFilter,magFilter:n.LinearFilter,format:n.RGBAFormat,type:n.HalfFloatType})],this.simulationMaterial=new n.ShaderMaterial({vertexShader:o,fragmentShader:"\nprecision highp float;\nvarying vec2 vUv;\nuniform sampler2D uPreviousState;\nuniform vec2 uResolution;\nuniform vec2 uMouse;\nuniform bool uIsMouseDown;\nuniform float uTime;\nuniform float uNoiseFactor;\nuniform float uBirthRate;\nuniform float uDeathRate;\nuniform float uSustainRate;\nuniform float uSpeed;\nuniform float uSampleRadius;\nuniform float uGrowthTarget;\nuniform float uMouseMass;\nuniform float uMouseRadius;\nuniform float uGaussianWeight;\n\nvec4 getNeighborhood(vec2 uv) {\n vec2 texel = 1.0 / uResolution;\n float alive = 0.0;\n float total = 0.0;\n float radius = uSampleRadius;\n float count = 0.0;\n \n for(float y = -radius; y <= radius; y++) {\n for(float x = -radius; x <= radius; x++) {\n vec2 offset = vec2(x, y) * texel;\n float dist = length(offset * uResolution / radius);\n if(dist <= radius) {\n float weight = exp(-dist * dist / uGaussianWeight);\n vec4 state = texture2D(uPreviousState, uv + offset);\n alive += state.r * weight;\n total += weight;\n count += 1.0;\n }\n }\n }\n alive = alive / total;\n float neighborFactor = 1.0 - abs(alive - 0.375) * 4.0;\n return vec4(alive, neighborFactor, count, total);\n}\n\nfloat hash(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nvoid main() {\n vec4 prevState = texture2D(uPreviousState, vUv);\n vec4 neighborhood = getNeighborhood(vUv);\n \n float currentMass = prevState.r;\n float currentVelocity = prevState.g;\n float currentHeight = prevState.b;\n \n float neighborFactor = neighborhood.y;\n float targetMass = 0.0;\n \n if(neighborFactor > uBirthRate) {\n targetMass = uGrowthTarget;\n } else if(neighborFactor < uDeathRate) {\n targetMass = 0.0;\n } else {\n targetMass = currentMass * uSustainRate;\n }\n \n float noise = hash(vUv + vec2(uTime * 0.01));\n float newMass = mix(currentMass, targetMass, uSpeed) + (noise - 0.5) * uNoiseFactor;\n float newVelocity = currentVelocity * 0.95;\n float newHeight = currentHeight * 0.98 + newMass * 0.5;\n \n vec2 mouseDist = vUv - uMouse;\n mouseDist.x *= uResolution.x / uResolution.y;\n\n if(uIsMouseDown && length(mouseDist) < uMouseRadius) {\n float distFactor = 1.0 - length(mouseDist) / uMouseRadius;\n float angle = atan(mouseDist.y, mouseDist.x);\n float variation = sin(angle * 8.0 + uTime * 2.0) * 0.3 + sin(angle * 4.0 - uTime * 3.0) * 0.2;\n newMass = uMouseMass + variation * distFactor * 0.3;\n newHeight += (0.3 + variation * 0.2) * distFactor;\n newVelocity += length(mouseDist) * (1.0 + variation) * 2.0;\n }\n \n newMass = clamp(newMass, 0.0, 1.0);\n newHeight = clamp(newHeight, 0.0, 1.0);\n \n gl_FragColor = vec4(newMass, newVelocity, newHeight, 1.0);\n}",uniforms:{uPreviousState:{value:null},uResolution:{value:new n.Vector2(this.sizes.width,this.sizes.height)},uMouse:{value:new n.Vector2(.5,.5)},uIsMouseDown:{value:!1},uTime:{value:0},uNoiseFactor:{value:.005},uBirthRate:{value:.9},uDeathRate:{value:.89},uSustainRate:{value:.985},uSpeed:{value:.1},uSampleRadius:{value:4},uGrowthTarget:{value:10},uMouseMass:{value:.6},uMouseRadius:{value:.05},uGaussianWeight:{value:8}}}),this.renderMaterial=new n.ShaderMaterial({vertexShader:o,fragmentShader:"\nprecision highp float;\nvarying vec2 vUv;\nuniform sampler2D uState;\nuniform float uTime;\nuniform vec3 uBaseColor;\nuniform vec4 uSecondaryColor;\nuniform float uRoughness;\nuniform float uMetalness;\nuniform sampler2D uImageTexture;\nuniform bool uGlassEffect;\nuniform float uToneMappingDenominator;\nuniform float uNormalMultiplier;\nuniform float uHeightMultiplier;\nuniform bool uShowImage;\nuniform bool uSmoothNormals;\nuniform float uLightPosZ;\nuniform vec3 uSpecularColor;\nuniform bool uSpotlightDampening;\nuniform float uNormalZComponent;\n\nvec3 calculateNormal(vec2 uv) {\n vec2 texel = vec2(1.0) / vec2(textureSize(uState, 0));\n float left = texture2D(uState, uv - vec2(texel.x, 0.0)).b;\n float right = texture2D(uState, uv + vec2(texel.x, 0.0)).b;\n float top = texture2D(uState, uv - vec2(0.0, texel.y)).b;\n float bottom = texture2D(uState, uv + vec2(0.0, texel.y)).b;\n \n vec3 normal = normalize(vec3(\n (right - left) * uNormalMultiplier,\n (bottom - top) * uNormalMultiplier,\n uNormalZComponent\n ));\n \n if(uSmoothNormals) {\n float height = texture2D(uState, uv).b;\n return mix(vec3(0.0, 0.0, 1.0), normal, smoothstep(0.1, 0.8, height * uHeightMultiplier));\n }\n return normal;\n}\n\nfloat ggxDistribution(float NdotH, float roughness) {\n float alpha = roughness * roughness;\n float alpha2 = alpha * alpha;\n float NdotH2 = NdotH * NdotH;\n float denom = NdotH2 * (alpha2 - 1.0) + 1.0;\n return alpha2 / (3.14159 * denom * denom);\n}\n\nfloat geometrySchlickGGX(float NdotV, float roughness) {\n float r = roughness + 1.0;\n float k = (r * r) / 8.0;\n return NdotV / (NdotV * (1.0 - k) + k);\n}\n\nvec3 fresnelSchlick(float cosTheta, vec3 F0) {\n return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);\n}\n\nvoid main() {\n vec4 state = texture2D(uState, vUv);\n float mass = state.r;\n float height = state.b;\n \n vec3 normal = calculateNormal(vUv);\n vec3 lightPos = vec3(2.0 * cos(uTime * 0.5), 2.0 * sin(uTime * 0.5), uLightPosZ);\n vec3 viewPos = vec3(0.0, 0.0, 2.0);\n vec3 worldPos = vec3(vUv * 2.0 - 1.0, height);\n \n vec3 N = normal;\n vec3 V = normalize(viewPos - worldPos);\n vec3 L = normalize(lightPos - worldPos);\n vec3 H = normalize(V + L);\n \n float NdotV = max(dot(N, V), 0.0);\n float NdotL = max(dot(N, L), 0.0);\n float NdotH = max(dot(N, H), 0.0);\n float HdotV = max(dot(H, V), 0.0);\n \n vec3 F0 = mix(uSpecularColor, uBaseColor, uMetalness);\n vec3 F = fresnelSchlick(HdotV, F0);\n float D = ggxDistribution(NdotH, uRoughness);\n float G = geometrySchlickGGX(NdotV, uRoughness) * geometrySchlickGGX(NdotL, uRoughness);\n \n vec3 specular = (D * F * G) / (4.0 * NdotV * NdotL + 0.001);\n specular *= uSpotlightDampening ? smoothstep(0.0, 0.3, height) : 1.0;\n \n vec3 kD = (vec3(1.0) - F) * (1.0 - uMetalness);\n vec3 diffuse = kD * uBaseColor / 3.14159;\n \n vec3 color = (diffuse + specular) * NdotL;\n \n if(uSecondaryColor.a > 0.0) {\n color -= uBaseColor * mass * 0.399;\n color += uSecondaryColor.rgb * uSecondaryColor.a * mass;\n } else {\n color += uBaseColor * mass * 0.399;\n }\n\n color += uBaseColor * 0.9;\n\n if(uGlassEffect) {\n vec2 distortedUV = vUv + normal.xy * height * 0.1;\n vec3 imageColor = texture2D(uImageTexture, distortedUV).rgb;\n float blendFactor = smoothstep(0.0, 0.8, mass * 0.7 + height * 0.5);\n vec3 distortedColor = mix(imageColor, color, blendFactor);\n\n if(uShowImage) {\n color = mix(distortedColor, color + imageColor * mass, blendFactor * 0.7);\n }\n \n if (blendFactor > 0.1) {\n vec2 aberrationOffset = normal.xy * height * 2.;\n vec3 redChannel = texture2D(uImageTexture, distortedUV + aberrationOffset).rgb;\n vec3 blueChannel = texture2D(uImageTexture, distortedUV - aberrationOffset).rgb;\n color.r = mix(color.r, redChannel.r, blendFactor * 0.3);\n color.b = mix(color.b, blueChannel.b, blendFactor * 0.3);\n }\n }\n \n color = color / (color + vec3(1.0));\n color = pow(color, vec3(1.0/uToneMappingDenominator));\n \n gl_FragColor = vec4(color, 1.0);\n}",uniforms:{uState:{value:null},uTime:{value:0},uBaseColor:{value:new n.Vector3(0,0,0)},uSecondaryColor:{value:new n.Vector4(0,.01,.02,1)},uRoughness:{value:.15},uMetalness:{value:.9},uImageTexture:{value:this.imageTexture},uShowImage:{value:!0},uGlassEffect:{value:!1},uToneMappingDenominator:{value:2.2},uSmoothNormals:{value:!1},uLightPosZ:{value:1.5},uSpecularColor:{value:new n.Vector3(.6,.6,.6)},uSpotlightDampening:{value:!0},uHeightMultiplier:{value:1.5},uNormalMultiplier:{value:4},uNormalZComponent:{value:.8}}}),this.initializeState()}initializeState(){const e=new Float32Array(this.sizes.width*this.sizes.height*4);for(let t=0;t<e.length;t+=4)e[t]=.1*Math.random(),e[t+1]=0,e[t+2]=0,e[t+3]=1;const t=new n.DataTexture(e,this.sizes.width,this.sizes.height,n.RGBAFormat,n.FloatType);t.needsUpdate=!0,this.renderer.setRenderTarget(this.renderTargets[0]);const i=new n.Mesh(this.quad,new n.MeshBasicMaterial({map:t})),a=new n.Scene;a.add(i),this.renderer.render(a,this.camera),this.renderer.setRenderTarget(null)}addEventListeners(){window.addEventListener("resize",this.onResize.bind(this)),window.addEventListener("mousemove",this.onMouseMove.bind(this)),this.canvas.addEventListener("touchstart",this.onTouchStart.bind(this),{passive:!1}),this.canvas.addEventListener("touchmove",this.onTouchMove.bind(this),{passive:!1}),this.canvas.addEventListener("touchend",this.onTouchEnd.bind(this),{passive:!1}),this.canvas.addEventListener("touchcancel",this.onTouchEnd.bind(this),{passive:!1}),this.canvas.style.touchAction="none",document.body.style.overscrollBehavior="none"}onResize(){this.renderer.setSize(window.innerWidth,window.innerHeight);const e=window.innerWidth/window.innerHeight;Math.round(this.options.baseResolution*e*this.options.stretchFactor)!==this.sizes.width&&this.resizeSimulation(this.options.baseResolution)}onMouseMove(e){if(!this.isTouch){const t=this.canvas.getBoundingClientRect(),i=(e.clientX-t.left)/t.width,n=1-(e.clientY-t.top)/t.height;this.simulationMaterial.uniforms.uMouse.value.x=Math.max(0,Math.min(1,i)),this.simulationMaterial.uniforms.uMouse.value.y=Math.max(0,Math.min(1,n)),this.simulationMaterial.uniforms.uIsMouseDown.value=!0,this.mouseTimeout&&clearTimeout(this.mouseTimeout),this.mouseTimeout=setTimeout((()=>{this.simulationMaterial.uniforms.uIsMouseDown.value=!1}),20)}}onTouchStart(e){e.preventDefault(),this.isTouch=!0,this.touchActive=!0,this.handleTouch(e.touches[0])}onTouchMove(e){e.preventDefault(),this.touchActive&&e.touches.length>0&&this.handleTouch(e.touches[0])}onTouchEnd(e){e.preventDefault(),this.touchActive=!1,this.simulationMaterial.uniforms.uIsMouseDown.value=!1}handleTouch(e){const t=this.canvas.getBoundingClientRect(),i=(e.clientX-t.left)/t.width,n=1-(e.clientY-t.top)/t.height;this.simulationMaterial.uniforms.uMouse.value.x=Math.max(0,Math.min(1,i)),this.simulationMaterial.uniforms.uMouse.value.y=Math.max(0,Math.min(1,n)),this.simulationMaterial.uniforms.uIsMouseDown.value=!0}animate(){requestAnimationFrame(this.animate.bind(this));const e=.001*performance.now();this.simulationMaterial.uniforms.uTime.value=e,this.renderMaterial.uniforms.uTime.value=e,this.simulationMaterial.uniforms.uPreviousState.value=this.renderTargets[0].texture,this.renderer.setRenderTarget(this.renderTargets[1]),this.renderer.render((new n.Scene).add(new n.Mesh(this.quad,this.simulationMaterial)),this.camera),this.renderMaterial.uniforms.uState.value=this.renderTargets[1].texture,this.renderer.setRenderTarget(null),this.renderer.render((new n.Scene).add(new n.Mesh(this.quad,this.renderMaterial)),this.camera),[this.renderTargets[0],this.renderTargets[1]]=[this.renderTargets[1],this.renderTargets[0]]}init(){this.initRenderer(),this.initScene(),this.initSimulation(),this.options.preset&&a[this.options.preset]&&a[this.options.preset](this),Object.keys(this.options.params).length>0&&this.updateSettings(this.options.params),this.addEventListeners(),this.options.showGui&&this.initGUI(),this.animate()}updateSettings(e){Object.entries(e).forEach((([e,t])=>{this.simulationMaterial.uniforms[e]&&(this.simulationMaterial.uniforms[e].value=t),this.renderMaterial.uniforms[e]&&(this.renderMaterial.uniforms[e].value=t)}))}applyPreset(e){a[e]&&a[e](this)}destroy(){this.renderer.dispose(),this.renderTargets.forEach((e=>e.dispose())),this.simulationMaterial.dispose(),this.renderMaterial.dispose(),window.removeEventListener("resize",this.onResize.bind(this)),this.canvas.remove()}initGUI(){if(!this.simulationMaterial||!this.renderMaterial||!window.GUI)return;const e=new window.GUI,t=e.addFolder("Simulation");t.add(this.simulationMaterial.uniforms.uNoiseFactor,"value",0,.1).name("Noise"),t.add(this.simulationMaterial.uniforms.uBirthRate,"value",0,1).name("Birth Rate"),t.add(this.simulationMaterial.uniforms.uDeathRate,"value",0,1).name("Death Rate"),t.add(this.simulationMaterial.uniforms.uSustainRate,"value",0,1).name("Sustain"),t.add(this.simulationMaterial.uniforms.uSpeed,"value",0,1).name("Speed");const i=e.addFolder("Appearance");i.add(this.renderMaterial.uniforms.uRoughness,"value",0,1).name("Roughness"),i.add(this.renderMaterial.uniforms.uMetalness,"value",0,1).name("Metalness"),i.add(this.renderMaterial.uniforms.uGlassEffect,"value").name("Glass Effect")}resizeSimulation(e){const t=window.innerWidth/window.innerHeight,i={width:Math.round(e*t*this.options.stretchFactor),height:e},a=[new n.WebGLRenderTarget(i.width,i.height,{minFilter:n.LinearFilter,magFilter:n.LinearFilter,format:n.RGBAFormat,type:n.HalfFloatType}),new n.WebGLRenderTarget(i.width,i.height,{minFilter:n.LinearFilter,magFilter:n.LinearFilter,format:n.RGBAFormat,type:n.HalfFloatType})];this.simulationMaterial.uniforms.uResolution.value.set(i.width,i.height),this.renderTargets.forEach((e=>e.dispose())),this.renderTargets=a,this.sizes=i,this.baseResolution=e}},e.presets=a}));