shader_fluid_distortion_rgbshift.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> 6 7 <title>Fluid Distortion Post-processing — Alien.js</title> 8 9 <link rel="preconnect" href="https://fonts.gstatic.com"> 10 <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono"> 11 <link rel="stylesheet" href="../assets/css/style.css"> 12 13 <script type="module"> 14 import { BloomCompositeMaterial, Color, ColorManagement, DirectionalLight, Fluid, Fog, GLSL3, Group, HemisphereLight, LinearSRGBColorSpace, LuminosityMaterial, MathUtils, Mesh, MeshBasicMaterial, MeshStandardMaterial, NoBlending, OrthographicCamera, PanelItem, PerspectiveCamera, PlaneGeometry, RawShaderMaterial, Reflector, RepeatWrapping, SVGLoader, Scene, TextureLoader, UI, UnrealBloomBlurMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer, getFullscreenTriangle, ticker } from '../../build/alien.three.js'; 15 16 // Based on https://github.com/PavelDoGreat/WebGL-Fluid-Simulation 17 // Based on https://oframe.github.io/ogl/examples/?src=post-fluid-distortion.html by gordonnl 18 19 import rgbshift from '../../src/shaders/modules/rgbshift/rgbshift.glsl.js'; 20 import dither from '../../src/shaders/modules/dither/dither.glsl.js'; 21 22 class CompositeMaterial extends RawShaderMaterial { 23 constructor() { 24 super({ 25 glslVersion: GLSL3, 26 uniforms: { 27 tScene: { value: null }, 28 tBloom: { value: null }, 29 tFluid: { value: null }, 30 uBloomDistortion: { value: 1.5 } 31 }, 32 vertexShader: /* glsl */ ` 33 in vec3 position; 34 in vec2 uv; 35 36 out vec2 vUv; 37 38 void main() { 39 vUv = uv; 40 41 gl_Position = vec4(position, 1.0); 42 } 43 `, 44 fragmentShader: /* glsl */ ` 45 precision highp float; 46 47 uniform sampler2D tScene; 48 uniform sampler2D tBloom; 49 uniform sampler2D tFluid; 50 uniform float uBloomDistortion; 51 52 in vec2 vUv; 53 54 out vec4 FragColor; 55 56 ${rgbshift} 57 ${dither} 58 59 void main() { 60 vec3 fluid = texture(tFluid, vUv).rgb; 61 vec2 uv = vUv - fluid.rg * 0.0002; 62 63 vec2 dir = 0.5 - vUv; 64 float angle = atan(dir.y, dir.x); 65 float amount = length(fluid.rg) * 0.0001; 66 67 FragColor = getRGB(tScene, uv, angle, amount); 68 69 FragColor.rgb += getRGB(tBloom, uv, angle, amount + 0.001 * uBloomDistortion).rgb; 70 71 FragColor.rgb = dither(FragColor.rgb); 72 FragColor.a = 1.0; 73 } 74 `, 75 blending: NoBlending, 76 depthTest: false, 77 depthWrite: false 78 }); 79 } 80 } 81 82 class Triangle extends Group { 83 constructor() { 84 super(); 85 } 86 87 async initMesh() { 88 const { camera, loadSVG } = WorldController; 89 90 const data = await loadSVG('data:image/svg+xml;utf8,<svg><path d="M 3 0 L 0 5 H 6 Z" stroke-width="0.25"/></svg>'); 91 const paths = data.paths; 92 93 const group = new Group(); 94 group.position.set(0, 1.4, -11); 95 group.scale.y *= -1; 96 group.lookAt(camera.position); 97 98 for (let i = 0, l = paths.length; i < l; i++) { 99 const path = paths[i]; 100 101 const material = new MeshBasicMaterial(); 102 103 for (let j = 0, jl = path.subPaths.length; j < jl; j++) { 104 const subPath = path.subPaths[j]; 105 const geometry = SVGLoader.pointsToStroke(subPath.getPoints(), path.userData.style); 106 107 if (geometry) { 108 geometry.center(); 109 110 const mesh = new Mesh(geometry, material); 111 group.add(mesh); 112 } 113 } 114 } 115 116 this.add(group); 117 } 118 119 ready = () => this.initMesh(); 120 } 121 122 class Floor extends Group { 123 constructor() { 124 super(); 125 126 this.initReflector(); 127 } 128 129 initReflector() { 130 this.reflector = new Reflector(); 131 } 132 133 async initMesh() { 134 const { loadTexture } = WorldController; 135 136 const geometry = new PlaneGeometry(100, 100); 137 138 // Second set of UVs for aoMap and lightMap 139 // https://threejs.org/docs/#api/en/materials/MeshStandardMaterial.aoMap 140 geometry.attributes.uv1 = geometry.attributes.uv; 141 142 // Textures 143 const [map, normalMap, ormMap] = await Promise.all([ 144 // loadTexture('uv.jpg'), 145 loadTexture('pbr/polished_concrete_basecolor.jpg'), 146 loadTexture('pbr/polished_concrete_normal.jpg'), 147 // https://occlusion-roughness-metalness.cyberspace.app/ 148 loadTexture('pbr/polished_concrete_orm.jpg') 149 ]); 150 151 map.wrapS = RepeatWrapping; 152 map.wrapT = RepeatWrapping; 153 map.repeat.set(16, 16); 154 155 normalMap.wrapS = RepeatWrapping; 156 normalMap.wrapT = RepeatWrapping; 157 normalMap.repeat.set(16, 16); 158 159 ormMap.wrapS = RepeatWrapping; 160 ormMap.wrapT = RepeatWrapping; 161 ormMap.repeat.set(16, 16); 162 163 const material = new MeshStandardMaterial({ 164 color: new Color().offsetHSL(0, 0, -0.8), 165 metalness: 1, 166 roughness: 1, 167 map, 168 metalnessMap: ormMap, 169 roughnessMap: ormMap, 170 aoMap: ormMap, 171 aoMapIntensity: 1, 172 normalMap, 173 normalScale: new Vector2(3, 3) 174 }); 175 176 // Second channel for aoMap and lightMap 177 // https://threejs.org/docs/#api/en/materials/MeshStandardMaterial.aoMap 178 material.aoMap.channel = 1; 179 180 const uniforms = { 181 mirror: { value: 0 }, 182 mixStrength: { value: 10 } 183 }; 184 185 material.onBeforeCompile = shader => { 186 shader.uniforms.reflectMap = this.reflector.renderTargetUniform; 187 shader.uniforms.textureMatrix = this.reflector.textureMatrixUniform; 188 189 shader.uniforms = Object.assign(shader.uniforms, uniforms); 190 191 shader.vertexShader = shader.vertexShader.replace( 192 'void main() {', 193 /* glsl */ ` 194 uniform mat4 textureMatrix; 195 196 out vec4 vCoord; 197 out vec3 vToEye; 198 199 void main() { 200 ` 201 ); 202 203 shader.vertexShader = shader.vertexShader.replace( 204 '#include <project_vertex>', 205 /* glsl */ ` 206 #include <project_vertex> 207 208 vCoord = textureMatrix * vec4(transformed, 1.0); 209 vToEye = cameraPosition - (modelMatrix * vec4(transformed, 1.0)).xyz; 210 ` 211 ); 212 213 shader.fragmentShader = shader.fragmentShader.replace( 214 'void main() {', 215 /* glsl */ ` 216 uniform sampler2D reflectMap; 217 uniform float mirror; 218 uniform float mixStrength; 219 220 in vec4 vCoord; 221 in vec3 vToEye; 222 223 void main() { 224 ` 225 ); 226 227 shader.fragmentShader = shader.fragmentShader.replace( 228 '#include <emissivemap_fragment>', 229 /* glsl */ ` 230 #include <emissivemap_fragment> 231 232 vec4 normalColor = texture(normalMap, vNormalMapUv * normalScale); 233 vec3 reflectNormal = normalize(vec3(normalColor.r * 2.0 - 1.0, normalColor.b, normalColor.g * 2.0 - 1.0)); 234 vec3 reflectCoord = vCoord.xyz / vCoord.w; 235 vec2 reflectUv = reflectCoord.xy + reflectCoord.z * reflectNormal.xz * 0.05; 236 vec4 reflectColor = texture(reflectMap, reflectUv); 237 238 // Fresnel 239 vec3 toEye = normalize(vToEye); 240 float theta = max(dot(toEye, normal), 0.0); 241 float reflectance = pow((1.0 - theta), 5.0); 242 243 reflectColor = mix(vec4(0), reflectColor, reflectance); 244 245 diffuseColor.rgb = diffuseColor.rgb * ((1.0 - min(1.0, mirror)) + reflectColor.rgb * mixStrength); 246 ` 247 ); 248 }; 249 250 const mesh = new Mesh(geometry, material); 251 mesh.position.y = -1.6; 252 mesh.rotation.x = -Math.PI / 2; 253 mesh.add(this.reflector); 254 255 mesh.onBeforeRender = (renderer, scene, camera) => { 256 this.visible = false; 257 this.reflector.update(renderer, scene, camera); 258 this.visible = true; 259 }; 260 261 this.add(mesh); 262 } 263 264 // Public methods 265 266 resize = (width, height) => { 267 width = Math.round(width / 2); 268 height = 1024; 269 270 this.reflector.setSize(width, height); 271 }; 272 273 ready = () => this.initMesh(); 274 } 275 276 class SceneView extends Group { 277 constructor() { 278 super(); 279 280 this.visible = false; 281 282 this.initViews(); 283 } 284 285 initViews() { 286 this.floor = new Floor(); 287 this.add(this.floor); 288 289 this.triangle = new Triangle(); 290 this.add(this.triangle); 291 } 292 293 // Public methods 294 295 resize = (width, height) => { 296 this.floor.resize(width, height); 297 }; 298 299 ready = () => Promise.all([ 300 this.floor.ready(), 301 this.triangle.ready() 302 ]); 303 } 304 305 class SceneController { 306 static init(view) { 307 this.view = view; 308 } 309 310 // Public methods 311 312 static resize = (width, height) => { 313 this.view.resize(width, height); 314 }; 315 316 static update = () => { 317 }; 318 319 static animateIn = () => { 320 this.view.visible = true; 321 }; 322 323 static ready = () => this.view.ready(); 324 } 325 326 class PanelController { 327 static init(ui) { 328 this.ui = ui; 329 330 this.initPanel(); 331 } 332 333 static initPanel() { 334 const { fluid, luminosityMaterial, bloomCompositeMaterial, compositeMaterial } = RenderManager; 335 336 const items = [ 337 { 338 name: 'FPS' 339 }, 340 { 341 type: 'divider' 342 }, 343 { 344 type: 'slider', 345 name: 'Iterate', 346 min: 0, 347 max: 10, 348 step: 1, 349 value: fluid.iterations, 350 callback: value => { 351 fluid.iterations = value; 352 } 353 }, 354 { 355 type: 'slider', 356 name: 'Density', 357 min: 0, 358 max: 1, 359 step: 0.01, 360 value: fluid.densityDissipation, 361 callback: value => { 362 fluid.densityDissipation = value; 363 } 364 }, 365 { 366 type: 'slider', 367 name: 'Velocity', 368 min: 0, 369 max: 1, 370 step: 0.01, 371 value: fluid.velocityDissipation, 372 callback: value => { 373 fluid.velocityDissipation = value; 374 } 375 }, 376 { 377 type: 'slider', 378 name: 'Pressure', 379 min: 0, 380 max: 1, 381 step: 0.01, 382 value: fluid.pressureDissipation, 383 callback: value => { 384 fluid.pressureDissipation = value; 385 } 386 }, 387 { 388 type: 'slider', 389 name: 'Curl', 390 min: 0, 391 max: 50, 392 step: 0.1, 393 value: fluid.curlStrength, 394 callback: value => { 395 fluid.curlStrength = value; 396 } 397 }, 398 { 399 type: 'slider', 400 name: 'Radius', 401 min: 0, 402 max: 1, 403 step: 0.01, 404 value: fluid.radius, 405 callback: value => { 406 fluid.radius = value; 407 } 408 }, 409 { 410 type: 'divider' 411 }, 412 { 413 type: 'slider', 414 name: 'Thresh', 415 min: 0, 416 max: 1, 417 step: 0.01, 418 value: luminosityMaterial.uniforms.uThreshold.value, 419 callback: value => { 420 luminosityMaterial.uniforms.uThreshold.value = value; 421 } 422 }, 423 { 424 type: 'slider', 425 name: 'Smooth', 426 min: 0, 427 max: 1, 428 step: 0.01, 429 value: luminosityMaterial.uniforms.uSmoothing.value, 430 callback: value => { 431 luminosityMaterial.uniforms.uSmoothing.value = value; 432 } 433 }, 434 { 435 type: 'slider', 436 name: 'Strength', 437 min: 0, 438 max: 2, 439 step: 0.01, 440 value: RenderManager.bloomStrength, 441 callback: value => { 442 RenderManager.bloomStrength = value; 443 bloomCompositeMaterial.uniforms.uBloomFactors.value = RenderManager.bloomFactors(); 444 } 445 }, 446 { 447 type: 'slider', 448 name: 'Radius', 449 min: 0, 450 max: 1, 451 step: 0.01, 452 value: RenderManager.bloomRadius, 453 callback: value => { 454 RenderManager.bloomRadius = value; 455 bloomCompositeMaterial.uniforms.uBloomFactors.value = RenderManager.bloomFactors(); 456 } 457 }, 458 { 459 type: 'slider', 460 name: 'Chroma', 461 min: 0, 462 max: 10, 463 step: 0.1, 464 value: compositeMaterial.uniforms.uBloomDistortion.value, 465 callback: value => { 466 compositeMaterial.uniforms.uBloomDistortion.value = value; 467 } 468 } 469 ]; 470 471 items.forEach(data => { 472 this.ui.addPanel(new PanelItem(data)); 473 }); 474 } 475 } 476 477 const BlurDirectionX = new Vector2(1, 0); 478 const BlurDirectionY = new Vector2(0, 1); 479 480 class RenderManager { 481 static init(renderer, scene, camera) { 482 this.renderer = renderer; 483 this.scene = scene; 484 this.camera = camera; 485 486 this.width = 1; 487 this.height = 1; 488 489 // Fluid simulation 490 this.lastMouse = new Vector2(); 491 492 // Unreal bloom 493 this.luminosityThreshold = 0.1; 494 this.luminositySmoothing = 1; 495 this.bloomStrength = 0.3; 496 this.bloomRadius = 0.2; 497 this.bloomDistortion = 1; 498 499 this.enabled = true; 500 501 this.initRenderer(); 502 503 this.addListeners(); 504 } 505 506 static initRenderer() { 507 const { screenTriangle, aspect } = WorldController; 508 509 // Fullscreen triangle 510 this.screenCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1); 511 this.screen = new Mesh(screenTriangle); 512 this.screen.frustumCulled = false; 513 514 // Render targets 515 this.renderTarget = new WebGLRenderTarget(1, 1, { 516 depthBuffer: false 517 }); 518 519 this.renderTargetBright = this.renderTarget.clone(); 520 this.renderTargetsHorizontal = []; 521 this.renderTargetsVertical = []; 522 this.nMips = 5; 523 524 for (let i = 0, l = this.nMips; i < l; i++) { 525 this.renderTargetsHorizontal.push(this.renderTarget.clone()); 526 this.renderTargetsVertical.push(this.renderTarget.clone()); 527 } 528 529 this.renderTarget.depthBuffer = true; 530 531 // Fluid simulation 532 this.fluid = new Fluid(this.renderer, { 533 curlStrength: 0 534 }); 535 this.fluid.splatMaterial.uniforms.uAspect = aspect; 536 537 // Luminosity high pass material 538 this.luminosityMaterial = new LuminosityMaterial(); 539 this.luminosityMaterial.uniforms.uThreshold.value = this.luminosityThreshold; 540 this.luminosityMaterial.uniforms.uSmoothing.value = this.luminositySmoothing; 541 542 // Separable Gaussian blur materials 543 this.blurMaterials = []; 544 545 const kernelSizeArray = [3, 5, 7, 9, 11]; 546 547 for (let i = 0, l = this.nMips; i < l; i++) { 548 this.blurMaterials.push(new UnrealBloomBlurMaterial(kernelSizeArray[i])); 549 } 550 551 // Unreal bloom composite material 552 this.bloomCompositeMaterial = new BloomCompositeMaterial(); 553 this.bloomCompositeMaterial.uniforms.tBlur1.value = this.renderTargetsVertical[0].texture; 554 this.bloomCompositeMaterial.uniforms.tBlur2.value = this.renderTargetsVertical[1].texture; 555 this.bloomCompositeMaterial.uniforms.tBlur3.value = this.renderTargetsVertical[2].texture; 556 this.bloomCompositeMaterial.uniforms.tBlur4.value = this.renderTargetsVertical[3].texture; 557 this.bloomCompositeMaterial.uniforms.tBlur5.value = this.renderTargetsVertical[4].texture; 558 this.bloomCompositeMaterial.uniforms.uBloomFactors.value = this.bloomFactors(); 559 560 // Composite material 561 this.compositeMaterial = new CompositeMaterial(); 562 this.compositeMaterial.uniforms.tFluid = this.fluid.uniform; 563 this.compositeMaterial.uniforms.uBloomDistortion.value = this.bloomDistortion; 564 } 565 566 static bloomFactors() { 567 const bloomFactors = [1, 0.8, 0.6, 0.4, 0.2]; 568 569 for (let i = 0, l = this.nMips; i < l; i++) { 570 const factor = bloomFactors[i]; 571 bloomFactors[i] = this.bloomStrength * MathUtils.lerp(factor, 1.2 - factor, this.bloomRadius); 572 } 573 574 return bloomFactors; 575 } 576 577 static addListeners() { 578 window.addEventListener('pointermove', this.onPointerMove); 579 } 580 581 // Event handlers 582 583 static onPointerMove = ({ clientX, clientY }) => { 584 if (!this.enabled) { 585 return; 586 } 587 588 const event = { 589 x: clientX, 590 y: clientY 591 }; 592 593 // First input 594 if (!this.lastMouse.isInit) { 595 this.lastMouse.isInit = true; 596 this.lastMouse.copy(event); 597 } 598 599 const deltaX = event.x - this.lastMouse.x; 600 const deltaY = event.y - this.lastMouse.y; 601 602 this.lastMouse.copy(event); 603 604 // Add if the mouse is moving 605 if (Math.abs(deltaX) || Math.abs(deltaY)) { 606 // Update fluid simulation inputs 607 this.fluid.splats.push({ 608 // Get mouse value in 0 to 1 range, with Y flipped 609 x: event.x / this.width, 610 y: 1 - event.y / this.height, 611 dx: deltaX * 5, 612 dy: deltaY * -5 613 }); 614 } 615 }; 616 617 // Public methods 618 619 static resize = (width, height, dpr) => { 620 this.width = width; 621 this.height = height; 622 623 this.renderer.setPixelRatio(dpr); 624 this.renderer.setSize(width, height); 625 626 width = Math.round(width * dpr); 627 height = Math.round(height * dpr); 628 629 this.renderTarget.setSize(width, height); 630 631 // Unreal bloom 632 width = Math.round(width / 2); 633 height = Math.round(height / 2); 634 635 this.renderTargetBright.setSize(width, height); 636 637 for (let i = 0, l = this.nMips; i < l; i++) { 638 this.renderTargetsHorizontal[i].setSize(width, height); 639 this.renderTargetsVertical[i].setSize(width, height); 640 641 this.blurMaterials[i].uniforms.uResolution.value.set(width, height); 642 643 width = Math.round(width / 2); 644 height = Math.round(height / 2); 645 } 646 }; 647 648 static update = () => { 649 const renderer = this.renderer; 650 const scene = this.scene; 651 const camera = this.camera; 652 653 if (!this.enabled) { 654 renderer.setRenderTarget(null); 655 renderer.render(scene, camera); 656 return; 657 } 658 659 const renderTarget = this.renderTarget; 660 const renderTargetBright = this.renderTargetBright; 661 const renderTargetsHorizontal = this.renderTargetsHorizontal; 662 const renderTargetsVertical = this.renderTargetsVertical; 663 664 // Perform all of the fluid simulation renders 665 this.fluid.update(); 666 667 // Scene pass 668 renderer.setRenderTarget(renderTarget); 669 renderer.render(scene, camera); 670 671 // Extract bright areas 672 this.luminosityMaterial.uniforms.tMap.value = renderTarget.texture; 673 this.screen.material = this.luminosityMaterial; 674 renderer.setRenderTarget(renderTargetBright); 675 renderer.render(this.screen, this.screenCamera); 676 677 // Blur all the mips progressively 678 let inputRenderTarget = renderTargetBright; 679 680 for (let i = 0, l = this.nMips; i < l; i++) { 681 this.screen.material = this.blurMaterials[i]; 682 683 this.blurMaterials[i].uniforms.tMap.value = inputRenderTarget.texture; 684 this.blurMaterials[i].uniforms.uDirection.value = BlurDirectionX; 685 renderer.setRenderTarget(renderTargetsHorizontal[i]); 686 renderer.render(this.screen, this.screenCamera); 687 688 this.blurMaterials[i].uniforms.tMap.value = this.renderTargetsHorizontal[i].texture; 689 this.blurMaterials[i].uniforms.uDirection.value = BlurDirectionY; 690 renderer.setRenderTarget(renderTargetsVertical[i]); 691 renderer.render(this.screen, this.screenCamera); 692 693 inputRenderTarget = renderTargetsVertical[i]; 694 } 695 696 // Composite all the mips 697 this.screen.material = this.bloomCompositeMaterial; 698 renderer.setRenderTarget(renderTargetsHorizontal[0]); 699 renderer.render(this.screen, this.screenCamera); 700 701 // Composite pass (render to screen) 702 this.compositeMaterial.uniforms.tScene.value = renderTarget.texture; 703 this.compositeMaterial.uniforms.tBloom.value = renderTargetsHorizontal[0].texture; 704 this.screen.material = this.compositeMaterial; 705 renderer.setRenderTarget(null); 706 renderer.render(this.screen, this.screenCamera); 707 }; 708 } 709 710 class CameraController { 711 static init(camera) { 712 this.camera = camera; 713 714 this.mouse = new Vector2(); 715 this.lookAt = new Vector3(0, 0, -2); 716 this.origin = new Vector3(); 717 this.target = new Vector3(); 718 this.targetXY = new Vector2(5, 1); 719 this.origin.copy(this.camera.position); 720 721 this.lerpSpeed = 0.02; 722 this.enabled = false; 723 724 this.addListeners(); 725 } 726 727 static addListeners() { 728 window.addEventListener('pointermove', this.onPointerMove); 729 } 730 731 // Event handlers 732 733 static onPointerMove = ({ clientX, clientY }) => { 734 if (!this.enabled) { 735 return; 736 } 737 738 this.mouse.x = (clientX / document.documentElement.clientWidth) * 2 - 1; 739 this.mouse.y = 1 - (clientY / document.documentElement.clientHeight) * 2; 740 }; 741 742 // Public methods 743 744 static resize = (width, height) => { 745 this.camera.aspect = width / height; 746 this.camera.updateProjectionMatrix(); 747 748 if (width < height) { 749 this.camera.position.z = 14; 750 } else { 751 this.camera.position.z = 10; 752 } 753 754 this.origin.z = this.camera.position.z; 755 756 this.camera.lookAt(this.lookAt); 757 }; 758 759 static update = () => { 760 if (!this.enabled) { 761 return; 762 } 763 764 this.target.x = this.origin.x + this.targetXY.x * this.mouse.x; 765 this.target.y = this.origin.y + this.targetXY.y * this.mouse.y; 766 this.target.z = this.origin.z; 767 768 this.camera.position.lerp(this.target, this.lerpSpeed); 769 this.camera.lookAt(this.lookAt); 770 }; 771 772 static animateIn = () => { 773 this.enabled = true; 774 }; 775 } 776 777 class WorldController { 778 static init() { 779 this.initWorld(); 780 this.initLights(); 781 this.initLoaders(); 782 783 this.addListeners(); 784 } 785 786 static initWorld() { 787 this.renderer = new WebGLRenderer({ 788 powerPreference: 'high-performance', 789 antialias: true 790 }); 791 792 // Output canvas 793 this.element = this.renderer.domElement; 794 795 // Disable color management 796 ColorManagement.enabled = false; 797 this.renderer.outputColorSpace = LinearSRGBColorSpace; 798 799 // 3D scene 800 this.scene = new Scene(); 801 this.scene.background = new Color(0x060606); 802 this.scene.fog = new Fog(this.scene.background, 1, 100); 803 this.camera = new PerspectiveCamera(30); 804 this.camera.near = 0.5; 805 this.camera.far = 40; 806 this.camera.position.z = 10; 807 this.camera.lookAt(this.scene.position); 808 809 // Global geometries 810 this.screenTriangle = getFullscreenTriangle(); 811 812 // Global uniforms 813 this.resolution = { value: new Vector2() }; 814 this.texelSize = { value: new Vector2() }; 815 this.aspect = { value: 1 }; 816 this.time = { value: 0 }; 817 this.frame = { value: 0 }; 818 } 819 820 static initLights() { 821 this.scene.add(new HemisphereLight(0x606060, 0x404040, 3)); 822 823 const light = new DirectionalLight(0xffffff, 2); 824 light.position.set(1, 1, 1); 825 this.scene.add(light); 826 } 827 828 static initLoaders() { 829 this.textureLoader = new TextureLoader(); 830 this.textureLoader.setPath('../assets/textures/'); 831 832 this.svgLoader = new SVGLoader(); 833 } 834 835 static addListeners() { 836 this.renderer.domElement.addEventListener('touchstart', this.onTouchStart); 837 } 838 839 // Event handlers 840 841 static onTouchStart = e => { 842 e.preventDefault(); 843 }; 844 845 // Public methods 846 847 static resize = (width, height, dpr) => { 848 width = Math.round(width * dpr); 849 height = Math.round(height * dpr); 850 851 this.resolution.value.set(width, height); 852 this.texelSize.value.set(1 / width, 1 / height); 853 this.aspect.value = width / height; 854 }; 855 856 static update = (time, delta, frame) => { 857 this.time.value = time; 858 this.frame.value = frame; 859 }; 860 861 // Global handlers 862 863 static getTexture = (path, callback) => this.textureLoader.load(path, callback); 864 865 static loadTexture = path => this.textureLoader.loadAsync(path); 866 867 static loadSVG = path => this.svgLoader.loadAsync(path); 868 } 869 870 class App { 871 static async init() { 872 this.initWorld(); 873 this.initViews(); 874 this.initControllers(); 875 876 this.addListeners(); 877 this.onResize(); 878 879 await SceneController.ready(); 880 881 this.initPanel(); 882 883 CameraController.animateIn(); 884 SceneController.animateIn(); 885 } 886 887 static initWorld() { 888 WorldController.init(); 889 document.body.appendChild(WorldController.element); 890 } 891 892 static initViews() { 893 this.view = new SceneView(); 894 WorldController.scene.add(this.view); 895 896 this.ui = new UI({ fps: true }); 897 this.ui.animateIn(); 898 document.body.appendChild(this.ui.element); 899 } 900 901 static initControllers() { 902 const { renderer, scene, camera } = WorldController; 903 904 CameraController.init(camera); 905 SceneController.init(this.view); 906 RenderManager.init(renderer, scene, camera); 907 } 908 909 static initPanel() { 910 PanelController.init(this.ui); 911 } 912 913 static addListeners() { 914 window.addEventListener('resize', this.onResize); 915 ticker.add(this.onUpdate); 916 ticker.start(); 917 } 918 919 // Event handlers 920 921 static onResize = () => { 922 const width = document.documentElement.clientWidth; 923 const height = document.documentElement.clientHeight; 924 const dpr = window.devicePixelRatio; 925 926 WorldController.resize(width, height, dpr); 927 CameraController.resize(width, height); 928 SceneController.resize(width, height); 929 RenderManager.resize(width, height, dpr); 930 }; 931 932 static onUpdate = (time, delta, frame) => { 933 WorldController.update(time, delta, frame); 934 CameraController.update(); 935 SceneController.update(); 936 RenderManager.update(time, delta, frame); 937 this.ui.update(); 938 }; 939 } 940 941 App.init(); 942 </script> 943 </head> 944 <body> 945 </body> 946 </html>