diff --git a/node/server/public/app.js b/node/server/public/app.js index f8676d3..5c17f87 100644 --- a/node/server/public/app.js +++ b/node/server/public/app.js @@ -8,18 +8,265 @@ import { UnrealBloomPass } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/exam import { GammaCorrectionShader } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/shaders/GammaCorrectionShader.js'; import { VignetteShader } from "https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/shaders/VignetteShader.js"; -// ---- Deine komplette Three.js Szenen-Logik hier! ---- -// Szene, Kamera, Renderer, Lights, Landscape, Spinner, Trees, usw. -// Foliage-Shader wie bisher -// ... +// ---- Basis Three.js Szene ---- +const scene = new THREE.Scene(); +const ASPECT = 3/2, SCALE = 15; +const hw = SCALE/2, hh = (SCALE/ASPECT)/2; +const camera = new THREE.OrthographicCamera(-hw, hw, hh, -hh, 0.1, 1000); +camera.position.set(0, -14.424, 20); +camera.rotation.set(THREE.MathUtils.degToRad(55), 0, 0); -// Ab hier NUR noch das WebSocket-System für die Spirits! +const container = document.getElementById('viewer'); +const renderer = new THREE.WebGLRenderer({ antialias:true }); +renderer.outputColorSpace = THREE.SRGBColorSpace; +renderer.shadowMap.enabled = true; +renderer.shadowMap.type = THREE.PCFSoftShadowMap; +container.appendChild(renderer.domElement); + +// ---- Foliage Shader ---- +const foliageTexture = new THREE.TextureLoader().load('assets/sprites/foliage.png'); +foliageTexture.colorSpace = THREE.SRGBColorSpace; +const FoliageOverlayShader = { + uniforms: { + 'tDiffuse': { value: null }, + 'tFoliage': { value: foliageTexture }, + 'opacity': { value: 1.0 } + }, + vertexShader: ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); + } + `, + fragmentShader: ` + uniform sampler2D tDiffuse; + uniform sampler2D tFoliage; + uniform float opacity; + varying vec2 vUv; + void main() { + vec4 base = texture2D(tDiffuse, vUv); + vec4 foliage = texture2D(tFoliage, vUv); + gl_FragColor = mix(base, vec4(foliage.rgb, base.a), foliage.a * opacity); + } + ` +}; +const foliageOverlayPass = new ShaderPass(FoliageOverlayShader); + +// ---- Postprocessing Stack ---- +const composer = new EffectComposer(renderer); +composer.addPass(new RenderPass(scene, camera)); +composer.addPass(foliageOverlayPass); +composer.addPass(new UnrealBloomPass( + new THREE.Vector2(container.clientWidth, container.clientHeight), + 0.8, 0.2, 0.4 +)); +const vignettePass = new ShaderPass(VignetteShader); +vignettePass.uniforms['offset'].value = 0.3; +vignettePass.uniforms['darkness'].value = 1.35; +composer.addPass(vignettePass); +composer.addPass(new ShaderPass(GammaCorrectionShader)); + +// ---- Resize Handler ---- +function onResize(){ + const W=container.clientWidth, H=container.clientHeight, winA=W/H; + let vw,vh,vx,vy; + if(winA>ASPECT){ + vh=H; vw=H*ASPECT; vx=(W-vw)/2; vy=0; + } else { + vw=W; vh=W/ASPECT; vx=0; vy=(H-vh)/2; + } + renderer.setSize(W,H); + renderer.setViewport(vx,vy,vw,vh); + renderer.setScissor(vx,vy,vw,vh); + renderer.setScissorTest(true); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1.2; + renderer.outputColorSpace = THREE.SRGBColorSpace; + composer.setSize(W, H); + const dpr = window.devicePixelRatio || 1; + renderer.setPixelRatio(dpr); + composer.setPixelRatio(dpr); + camera.updateProjectionMatrix(); +} +window.addEventListener('resize',onResize); +onResize(); + +// ---- Environment Map (HDRI) ---- +const texLoader = new THREE.TextureLoader(); +const pmremGen = new THREE.PMREMGenerator(renderer); +texLoader.load('assets/hdri/environment.jpg', tex => { + const envRT = pmremGen.fromEquirectangular(tex).texture; + scene.environment = envRT; + scene.background = envRT; + tex.dispose(); + pmremGen.dispose(); +}); + +// ---- Shadow-Only-Material ---- +const shadowOnlyMaterial = new THREE.MeshBasicMaterial({ + color: 0x000000, + opacity: 0.01, + transparent: true, + depthWrite: false +}); + +// ---- Directional Sun ---- +const sun = new THREE.DirectionalLight(0xFFA230, 2); +sun.position.set(21, -25, 30); +sun.castShadow = true; +sun.shadow.mapSize.width = 2048; +sun.shadow.mapSize.height = 2048; +sun.shadow.camera.near = 1; +sun.shadow.camera.far = 100; +sun.shadow.camera.left = -30; +sun.shadow.camera.right = 30; +sun.shadow.camera.top = 30; +sun.shadow.camera.bottom = -30; +scene.add(sun); + +// ---- GLTF/DRACO Loader ---- +const draco = new DRACOLoader(); +draco.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/'); +const gltfLoader = new GLTFLoader(); +gltfLoader.setDRACOLoader(draco); + +// ---- Utility: GLBs laden ---- +async function loadGLB(path, pos, rotDeg, {receiveShadow=false, castShadow=false, emissive=null, visible=true, shadowOnly=false} = {}) { + const { scene: obj } = await gltfLoader.loadAsync(path); + obj.position.set(pos[0], pos[1], pos[2]); + obj.rotation.set( + THREE.MathUtils.degToRad(rotDeg[0]), + THREE.MathUtils.degToRad(rotDeg[1]), + THREE.MathUtils.degToRad(rotDeg[2]) + ); + obj.traverse(c => { + c.visible = visible; + if (c.isMesh) { + c.castShadow = castShadow; + c.receiveShadow = receiveShadow; + if (shadowOnly) c.material = shadowOnlyMaterial; + if (emissive && c.material && c.material.isMeshStandardMaterial) { + c.material.emissive = new THREE.Color(emissive); + c.material.emissiveIntensity = 1.0; + } + } + }); + scene.add(obj); + return obj; +} + +// ---- Spinner laden ---- +async function loadSpinner(path, pos, rotDeg, color, opacity) { + const { scene: obj } = await gltfLoader.loadAsync(path); + obj.position.set(...pos); + obj.rotation.set( + THREE.MathUtils.degToRad(rotDeg[0]), + THREE.MathUtils.degToRad(rotDeg[1]), + THREE.MathUtils.degToRad(rotDeg[2]) + ); + obj.traverse(c => { + c.visible = true; + if (c.isMesh && c.material && c.material.isMeshStandardMaterial) { + c.material.transparent = true; + c.material.opacity = opacity; + c.material.emissive = new THREE.Color(color); + c.material.emissiveIntensity = 3.0; + c.material.vertexColors = true; + c.castShadow = true; + } + }); + scene.add(obj); + return obj; +} + +// ---- Initialisiere Grundszene (ohne Spirits) ---- +let spinnerRed, spinnerBlue, torigate, landscape, shadowTree; +const rotatingLights = [], counterRotatingLights = []; +const LIGHT_RADIUS = 1; +const clock = new THREE.Clock(); + +(async()=>{ + landscape = await loadGLB( + 'assets/models/landscape.glb', + [0,0,0], [90,0,0], + {receiveShadow:true, castShadow:true} + ); + torigate = await loadGLB( + 'assets/models/tori.glb', + [0,6.59,0.375], [90,0,0], + {receiveShadow:true, castShadow:true} + ); + spinnerRed = await loadSpinner( + 'assets/models/spinner_red.glb', [0,16.55,0.88], [90,0,0], "#ff3333", 0.2 + ); + spinnerBlue = await loadSpinner( + 'assets/models/spinner_blue.glb', [0,16.55,0.88], [90,0,0], "#3380ff", 0.2 + ); + shadowTree = await loadGLB( + 'assets/models/tree_low.glb', + [0.0,0.0,0.0], [90,0,0], + {receiveShadow:false, castShadow:true, shadowOnly: true} + ); + + // Rotierende Lichter initialisieren + for(let i=0;i<3;i++){ + const L = new THREE.PointLight(0xFFA230, 5, 30); + L.castShadow = true; + rotatingLights.push(L); + scene.add(L); + + const L2 = new THREE.PointLight(0xFFA230, 5, 30); + L2.castShadow = true; + counterRotatingLights.push(L2); + scene.add(L2); + } + + animate(); +})(); + +// ---- Render-Loop ---- +function animate(){ + const dt = clock.getDelta(), t = clock.getElapsedTime(); + // Spinner Animation + const bob = Math.sin(t*1.2)*0.5; + const baseY = 16.55 + bob; + if (spinnerRed && spinnerBlue) { + spinnerRed.position.y = baseY + 0.8; + spinnerBlue.position.y = baseY; + spinnerRed.rotation.y -= 1.2 * dt; + spinnerBlue.rotation.y += 1.2 * dt; + } + // Rotierende Lichter + const center = new THREE.Vector3(0, 16.55, 1.5); + const lightZ = center.z; + for(let i=0; i { const msg = JSON.parse(event.data); if (msg.type === 'spirit') { @@ -28,27 +275,42 @@ ws.addEventListener('message', async (event) => { }); async function showSpirit(spirit) { + // Vorherigen Spirit entfernen if (currentSpiritGroup) { scene.remove(currentSpiritGroup); - // Dispose-Logik, falls notwendig + // Optional: Materialien etc. dispose() aufrufen + currentSpiritGroup = null; } - - const gltfLoader = new GLTFLoader(); - // Draco Loader, falls gebraucht: ... + // Modell laden const { scene: spiritObj } = await gltfLoader.loadAsync(spirit.modelUrl); spiritObj.traverse(mesh => { if (mesh.isMesh) { mesh.castShadow = true; mesh.receiveShadow = true; - // ... weitere Material-Settings + // weitere Material-Anpassungen... } }); - - spiritObj.position.set(0, 0, 0); // ggf. gewünschte Position + // Positionierung, z.B. am Boden leicht nach hinten + spiritObj.position.set(0, 0, -0.6); scene.add(spiritObj); currentSpiritGroup = spiritObj; + // Overlay mit Spirit-Name/Description updateSpiritOverlay(spirit); } -// ... restlicher Code ... \ No newline at end of file +// Overlay für Spirit-Infos +function updateSpiritOverlay(spirit) { + let el = document.getElementById('spirit-info'); + if (!el) { + el = document.createElement('div'); + el.id = 'spirit-info'; + el.style = ` + position:absolute; left:20px; top:20px; color:white; + background:rgba(0,0,0,0.6); padding:10px 18px; border-radius:10px; + font-family: sans-serif; z-index:10; max-width: 320px; + `; + document.body.appendChild(el); + } + el.innerHTML = `${spirit.name || 'Spirit'}
${spirit.desc || ""}`; +} \ No newline at end of file