| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| import {LitElement, css, html} from 'lit'; |
| import {customElement, property} from 'lit/decorators.js'; |
| import {Analyser} from './analyser'; |
|
|
| import * as THREE from 'three'; |
| import {EXRLoader} from 'three/addons/loaders/EXRLoader.js'; |
| import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js'; |
| import {RenderPass} from 'three/addons/postprocessing/RenderPass.js'; |
| import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js'; |
| import {UnrealBloomPass} from 'three/addons/postprocessing/UnrealBloomPass.js'; |
| import {FXAAShader} from 'three/addons/shaders/FXAAShader.js'; |
| import {fs as backdropFS, vs as backdropVS} from './backdrop-shader'; |
| import {vs as sphereVS} from './sphere-shader'; |
|
|
| |
| |
| |
| @customElement('gdm-live-audio-visuals-3d') |
| export class GdmLiveAudioVisuals3D extends LitElement { |
| private inputAnalyser!: Analyser; |
| private outputAnalyser!: Analyser; |
| private camera!: THREE.PerspectiveCamera; |
| private backdrop!: THREE.Mesh; |
| private composer!: EffectComposer; |
| private sphere!: THREE.Mesh; |
| private prevTime = 0; |
| private rotation = new THREE.Vector3(0, 0, 0); |
|
|
| private _outputNode!: AudioNode; |
|
|
| @property() |
| set outputNode(node: AudioNode) { |
| this._outputNode = node; |
| this.outputAnalyser = new Analyser(this._outputNode); |
| } |
|
|
| get outputNode() { |
| return this._outputNode; |
| } |
|
|
| private _inputNode!: AudioNode; |
|
|
| @property() |
| set inputNode(node: AudioNode) { |
| this._inputNode = node; |
| this.inputAnalyser = new Analyser(this._inputNode); |
| } |
|
|
| get inputNode() { |
| return this._inputNode; |
| } |
|
|
| private canvas!: HTMLCanvasElement; |
|
|
| static styles = css` |
| canvas { |
| width: 100% !important; |
| height: 100% !important; |
| position: absolute; |
| inset: 0; |
| image-rendering: pixelated; |
| } |
| `; |
|
|
| connectedCallback() { |
| super.connectedCallback(); |
| } |
|
|
| private init() { |
| const scene = new THREE.Scene(); |
| scene.background = new THREE.Color(0x100c14); |
|
|
| const backdrop = new THREE.Mesh( |
| new THREE.IcosahedronGeometry(10, 5), |
| new THREE.RawShaderMaterial({ |
| uniforms: { |
| resolution: {value: new THREE.Vector2(1, 1)}, |
| rand: {value: 0}, |
| }, |
| vertexShader: backdropVS, |
| fragmentShader: backdropFS, |
| glslVersion: THREE.GLSL3, |
| }), |
| ); |
| backdrop.material.side = THREE.BackSide; |
| scene.add(backdrop); |
| this.backdrop = backdrop; |
|
|
| const camera = new THREE.PerspectiveCamera( |
| 75, |
| window.innerWidth / window.innerHeight, |
| 0.1, |
| 1000, |
| ); |
| camera.position.set(2, -2, 5); |
| this.camera = camera; |
|
|
| const renderer = new THREE.WebGLRenderer({ |
| canvas: this.canvas, |
| antialias: !true, |
| }); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| renderer.setPixelRatio(window.devicePixelRatio / 1); |
|
|
| const geometry = new THREE.IcosahedronGeometry(1, 10); |
|
|
| new EXRLoader().load('piz_compressed.exr', (texture: THREE.Texture) => { |
| texture.mapping = THREE.EquirectangularReflectionMapping; |
| const exrCubeRenderTarget = pmremGenerator.fromEquirectangular(texture); |
| sphereMaterial.envMap = exrCubeRenderTarget.texture; |
| sphere.visible = true; |
| }); |
|
|
| const pmremGenerator = new THREE.PMREMGenerator(renderer); |
| pmremGenerator.compileEquirectangularShader(); |
|
|
| const sphereMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x000010, |
| metalness: 0.5, |
| roughness: 0.1, |
| emissive: 0x000010, |
| emissiveIntensity: 1.5, |
| }); |
|
|
| sphereMaterial.onBeforeCompile = (shader) => { |
| shader.uniforms.time = {value: 0}; |
| shader.uniforms.inputData = {value: new THREE.Vector4()}; |
| shader.uniforms.outputData = {value: new THREE.Vector4()}; |
|
|
| sphereMaterial.userData.shader = shader; |
|
|
| shader.vertexShader = sphereVS; |
| }; |
|
|
| const sphere = new THREE.Mesh(geometry, sphereMaterial); |
| scene.add(sphere); |
| sphere.visible = false; |
|
|
| this.sphere = sphere; |
|
|
| const renderPass = new RenderPass(scene, camera); |
|
|
| const bloomPass = new UnrealBloomPass( |
| new THREE.Vector2(window.innerWidth, window.innerHeight), |
| 5, |
| 0.5, |
| 0, |
| ); |
|
|
| const fxaaPass = new ShaderPass(FXAAShader); |
|
|
| const composer = new EffectComposer(renderer); |
| composer.addPass(renderPass); |
| |
| composer.addPass(bloomPass); |
|
|
| this.composer = composer; |
|
|
| function onWindowResize() { |
| camera.aspect = window.innerWidth / window.innerHeight; |
| camera.updateProjectionMatrix(); |
| const dPR = renderer.getPixelRatio(); |
| const w = window.innerWidth; |
| const h = window.innerHeight; |
| backdrop.material.uniforms.resolution.value.set(w * dPR, h * dPR); |
| renderer.setSize(w, h); |
| composer.setSize(w, h); |
| fxaaPass.material.uniforms['resolution'].value.set( |
| 1 / (w * dPR), |
| 1 / (h * dPR), |
| ); |
| } |
|
|
| window.addEventListener('resize', onWindowResize); |
| onWindowResize(); |
|
|
| this.animation(); |
| } |
|
|
| private animation() { |
| requestAnimationFrame(() => this.animation()); |
|
|
| this.inputAnalyser.update(); |
| this.outputAnalyser.update(); |
|
|
| const t = performance.now(); |
| const dt = (t - this.prevTime) / (1000 / 60); |
| this.prevTime = t; |
| const backdropMaterial = this.backdrop.material as THREE.RawShaderMaterial; |
| const sphereMaterial = this.sphere.material as THREE.MeshStandardMaterial; |
|
|
| backdropMaterial.uniforms.rand.value = Math.random() * 10000; |
|
|
| if (sphereMaterial.userData.shader) { |
| this.sphere.scale.setScalar( |
| 1 + (0.2 * this.outputAnalyser.data[1]) / 255, |
| ); |
|
|
| const f = 0.001; |
| this.rotation.x += (dt * f * 0.5 * this.outputAnalyser.data[1]) / 255; |
| this.rotation.z += (dt * f * 0.5 * this.inputAnalyser.data[1]) / 255; |
| this.rotation.y += (dt * f * 0.25 * this.inputAnalyser.data[2]) / 255; |
| this.rotation.y += (dt * f * 0.25 * this.outputAnalyser.data[2]) / 255; |
|
|
| const euler = new THREE.Euler( |
| this.rotation.x, |
| this.rotation.y, |
| this.rotation.z, |
| ); |
| const quaternion = new THREE.Quaternion().setFromEuler(euler); |
| const vector = new THREE.Vector3(0, 0, 5); |
| vector.applyQuaternion(quaternion); |
| this.camera.position.copy(vector); |
| this.camera.lookAt(this.sphere.position); |
|
|
| sphereMaterial.userData.shader.uniforms.time.value += |
| (dt * 0.1 * this.outputAnalyser.data[0]) / 255; |
| sphereMaterial.userData.shader.uniforms.inputData.value.set( |
| (1 * this.inputAnalyser.data[0]) / 255, |
| (0.1 * this.inputAnalyser.data[1]) / 255, |
| (10 * this.inputAnalyser.data[2]) / 255, |
| 0, |
| ); |
| sphereMaterial.userData.shader.uniforms.outputData.value.set( |
| (2 * this.outputAnalyser.data[0]) / 255, |
| (0.1 * this.outputAnalyser.data[1]) / 255, |
| (10 * this.outputAnalyser.data[2]) / 255, |
| 0, |
| ); |
| } |
|
|
| this.composer.render(); |
| } |
|
|
| protected firstUpdated() { |
| this.canvas = this.shadowRoot!.querySelector('canvas') as HTMLCanvasElement; |
| this.init(); |
| } |
|
|
| protected render() { |
| return html`<canvas></canvas>`; |
| } |
| } |
|
|
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gdm-live-audio-visuals-3d': GdmLiveAudioVisuals3D; |
| } |
| } |
|
|