import * as THREE from 'three'; import { GLTFLoader } from 'GLTFLoader'; import { DRACOLoader } from 'DRACOLoader'; import { EffectComposer } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/postprocessing/EffectComposer.js'; import { RenderPass } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/postprocessing/RenderPass.js'; import { ShaderPass } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/postprocessing/ShaderPass.js'; import { UnrealBloomPass } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/postprocessing/UnrealBloomPass.js'; 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"; // ---- 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); 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') { await showSpirit(msg.data); } }); async function showSpirit(spirit) { // Vorherigen Spirit entfernen if (currentSpiritGroup) { scene.remove(currentSpiritGroup); // Optional: Materialien etc. dispose() aufrufen currentSpiritGroup = null; } console.log("Lade Spirit", spirit.modelUrl); // Modell laden const { scene: spiritObj } = await gltfLoader.loadAsync(spirit.modelUrl); spiritObj.traverse(mesh => { if (mesh.isMesh) { mesh.castShadow = true; mesh.receiveShadow = true; // weitere Material-Anpassungen... } }); console.log("Spirit wurde der Szene hinzugefügt!", spiritObj); // Positionierung, z.B. am Boden leicht nach hinten spiritObj.position.set(0, 0, 0); scene.add(spiritObj); currentSpiritGroup = spiritObj; // Overlay mit Spirit-Name/Description updateSpiritOverlay(spirit); } // 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 || ""}`; }