/ examples / three / shader_fluid_distortion_rgbshift.html
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>