diff --git a/index.html b/index.html index 2450dba..b3c6a97 100644 --- a/index.html +++ b/index.html @@ -32,10 +32,15 @@ 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"; + import { ShaderPass } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/postprocessing/ShaderPass.js'; // damit kann man auch easy webgl shader hier reinschreiben + import { UnrealBloomPass } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/postprocessing/UnrealBloomPass.js'; // EPICCC .. + import { BrightnessContrastShader } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/shaders/BrightnessContrastShader.js'; + import { GammaCorrectionShader } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/shaders/GammaCorrectionShader.js'; + import { SSAOPass } from 'https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/postprocessing/SSAOPass.js'; + + import { FilmPass } from "https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/postprocessing/FilmPass.js"; + import { VignetteShader } from "https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/shaders/VignetteShader.js"; + import { RGBShiftShader } from "https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/shaders/RGBShiftShader.js"; // --- Szene / Kamera / Renderer --- const scene = new THREE.Scene(); @@ -48,18 +53,20 @@ 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 + //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 } + 'opacity': { value: 1.0 } // 0 = aus, 1 = voll }, vertexShader: ` varying vec2 vUv; @@ -76,27 +83,81 @@ void main() { vec4 base = texture2D(tDiffuse, vUv); vec4 foliage = texture2D(tFoliage, vUv); + // Normales Overlay: Foliage-Alpha mischt drüber gl_FragColor = mix(base, vec4(foliage.rgb, base.a), foliage.a * opacity); } ` }; - const foliageOverlayPass = new ShaderPass(FoliageOverlayShader); - // Postprocessing + const foliageOverlayPass = new ShaderPass(FoliageOverlayShader); + foliageOverlayPass.uniforms['tFoliage'].value = foliageTexture; + foliageOverlayPass.uniforms['opacity'].value = 1.0; // ggf. anpassen + + + + // post processing stack const composer = new EffectComposer(renderer); + + //SSAO + const ssaoPass = new SSAOPass(scene, camera, container.clientWidth, container.clientHeight); + ssaoPass.kernelRadius = 16; // Radius des AO-Effekts (6-16 experimentieren!) + ssaoPass.minDistance = 0.05; // Wie nah am Geometriepunkt AO startet + ssaoPass.maxDistance = 0.2; // Maximalweite (0.1–0.3 sieht am realistischsten aus) + ssaoPass.output = SSAOPass.OUTPUT.Default; // Normal/Default + + ssaoPass.bias = 0.05; // "Füllung" (0.01–0.1) + ssaoPass.aoClamp = 0.5; // Kontrast/Abdunklung (0.1–1.0) + ssaoPass.lumInfluence = 1; // Wie stark das Umgebungslicht AO beeinflusst + ssaoPass.intensity = 1.7; // Hauptstärke des Effekts (1.0–3.0!) + + //BLOOM + const bloomPass = new UnrealBloomPass( + new THREE.Vector2(container.clientWidth, container.clientHeight), + 0.8, // strength (1.0 ist gut, 0.3-2.0 experimentieren!) + 0.2, // radius (0.5-1.0) + 0.4 // threshold (alles, was heller als 0.0 ist, kann blühen) + ); + + //BRIGHTNESS-CONTRAST + const brightnessContrastPass = new ShaderPass(BrightnessContrastShader); + brightnessContrastPass.uniforms['brightness'].value = 0.25; // -1 bis +1 + brightnessContrastPass.uniforms['contrast'].value = 0.51; // -1 bis +1 (0.2-0.7 sieht oft gut aus!) + + //VIGNETTE-PASS + const vignettePass = new ShaderPass(VignetteShader); + vignettePass.uniforms['offset'].value = 0.3; // 1.0-2.0, experimentieren! + vignettePass.uniforms['darkness'].value = 1.35; // 1.0-2.5, je nach Mood + + //RBG SHIFT (Chromatic Aberration) + const rgbShiftPass = new ShaderPass(RGBShiftShader); + rgbShiftPass.uniforms['amount'].value = 0.001; // Sehr subtil! (0.001–0.003) + + //FILMPASS (Scanlines) + const filmPass = new FilmPass( + 1.15, // noise intensity + 0.125, // scanline intensity + 600, // scanline count + false // grayscale + ); + + + const gammaPass = new ShaderPass(GammaCorrectionShader); + + 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(ssaoPass); + composer.addPass(bloomPass); + //composer.addPass(brightnessContrastPass); composer.addPass(vignettePass); - composer.addPass(new ShaderPass(GammaCorrectionShader)); + composer.addPass(rgbShiftPass); + //composer.addPass(filmPass); + composer.addPass(gammaPass); + + + // end of postprocessing + - // Resize-Handler function onResize(){ const W=container.clientWidth, H=container.clientHeight, winA=W/H; let vw,vh,vx,vy; @@ -110,9 +171,10 @@ renderer.setScissor(vx,vy,vw,vh); renderer.setScissorTest(true); renderer.toneMapping = THREE.ACESFilmicToneMapping; - renderer.toneMappingExposure = 1.2; + renderer.toneMappingExposure = 1.2; // Belichtung erhöhen/niedriger testen! renderer.outputColorSpace = THREE.SRGBColorSpace; composer.setSize(W, H); + if (ssaoPass) ssaoPass.setSize(W, H); const dpr = window.devicePixelRatio || 1; renderer.setPixelRatio(dpr); composer.setPixelRatio(dpr); @@ -124,7 +186,7 @@ // --- HDRI Environment --- const texLoader = new THREE.TextureLoader(); const pmremGen = new THREE.PMREMGenerator(renderer); - texLoader.load('assets/hdri/environment.jpg', tex => { + texLoader.load('assets/hdri/environment.png', tex => { const envRT = pmremGen.fromEquirectangular(tex).texture; scene.environment = envRT; scene.background = envRT; @@ -133,8 +195,11 @@ }); // --- Schatten-gebendes Licht --- + //scene.add(new THREE.AmbientLight(0xFFA230, 0.25)); const sun = new THREE.DirectionalLight(0xFFA230, 2); sun.position.set(21, -25, 30); + //sun.position.set(10, -10, 20); + sun.castShadow = true; sun.shadow.mapSize.width = 2048; sun.shadow.mapSize.height = 2048; @@ -164,7 +229,7 @@ .map(name=>`assets/models/spirits/${name}`); } - // --- GLBs laden --- + // --- GLBs laden (kein Axis Swap) --- 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]); @@ -174,7 +239,7 @@ THREE.MathUtils.degToRad(rotDeg[2]) ); obj.traverse(c => { - c.visible = visible; + c.visible = visible; // MUSS true sein, sonst kein Schatten! if (c.isMesh) { c.castShadow = castShadow; c.receiveShadow = receiveShadow; @@ -189,16 +254,8 @@ return obj; } - // --- Shadow-Only-Material --- - const shadowOnlyMaterial = new THREE.MeshBasicMaterial({ - color: 0x000000, - opacity: 0.01, - transparent: true, - depthWrite: false - }); - // --- Variablen für Animation --- - let spinnerRed, spinnerBlue, torigate, landscape, shadowTree; + let spinnerRed, spinnerBlue, torigate, environment, shadowTree; const clock = new THREE.Clock(); let lastSpawn = 0; const SPAWN_INT = 5; @@ -209,77 +266,217 @@ const rotatingLights = []; const counterRotatingLights = []; const LIGHT_RADIUS = 1; - 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); +for(let i=0;i<3;i++){ + const L = new THREE.PointLight(0xFFA230, 5, 30); // Intensität runter, Reichweite hoch + 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); +} + + // Teleport-Flash-Material + const flashMaterial = new THREE.MeshBasicMaterial({ + color: 0xffffcc, + transparent: true, + opacity: 0.95, + blending: THREE.AdditiveBlending, + depthWrite: false + }); + + // Shadow-Only-Material + const shadowOnlyMaterial = new THREE.MeshBasicMaterial({ + color: 0x000000, + opacity: 0.01, + transparent: true, + depthWrite: false + }); + + +// === Partikel-Textur vorbereiten (einfacher weißer Kreis) === +const particleTexture = new THREE.TextureLoader().load('assets/sprites/particle.png'); +particleTexture.colorSpace = THREE.SRGBColorSpace; +// === Audio vorbereiten === +/* +const listener = new THREE.AudioListener(); +camera.add(listener); +const soundBufferPromise = fetch('assets/sound/spawn.ogg') + .then(r=>r.arrayBuffer()) + .then(buf=>new Promise(res=>{ + const audio = new AudioContext(); + audio.decodeAudioData(buf, res); + })); +*/ + +// === Neue Spirit-Klasse === +class Spirit { + constructor(position) { + this.clock = new THREE.Clock(); + this.grp = new THREE.Group(); + this.spiritMeshes = []; + this.isFading = true; // Für den Material-Überblend + // Positionieren (auf Boden, dann +1m für Partikeleffekt) + this.grp.position.copy(position); + this.grp.position.z -= 0.6; + scene.add(this.grp); + + // Partikel-Spawn + this.particles = this.makeParticles(); + this.particles.position.y = 1.0; // Mitte Spirit + this.grp.add(this.particles); + + // Sound abspielen (async, aber egal) + /* + soundBufferPromise.then(buffer => { + const sfx = new THREE.Audio(listener); + sfx.setBuffer(buffer); + sfx.setVolume(0.7); + sfx.play(); + // Sound ist "virtuell" an Spirit-Gruppe gebunden, keine Entfernungsmischung + }); + */ + + // Laden Spirit-GLB + const path = spiritModels[Math.floor(Math.random()*spiritModels.length)]; + gltfLoader.load(path, ({ scene: s }) => { + s.traverse(mesh => { + if(mesh.isMesh){ + mesh.castShadow = true; + mesh.receiveShadow = true; + // Originalmaterial speichern + mesh.userData.originalMaterial = mesh.material.clone(); + mesh.material = mesh.material.clone(); + // Direkt auf Flash-Farbe setzen, transparent! + mesh.material.color.set(0xffffcc); + //mesh.material.transparent = true; + mesh.material.opacity = 0.0; + mesh.material.emissive?.set(0xffffcc); // Falls vorhanden + mesh.material.emissiveIntensity = 2.0; + this.spiritMeshes.push(mesh); + } + }); + s.rotation.x = -Math.PI; + this.grp.add(s); + }); + } + + // === Partikel-Effekt erstellen === + makeParticles() { + const N = 36; // Partikelanzahl + const geom = new THREE.BufferGeometry(); + const positions = []; + const velocities = []; + for(let i=0; i this.particles.userData.lifetime){ + this.grp.remove(this.particles); + this.particles.geometry.dispose(); + this.particles.material.dispose(); + this.particles = null; + } } - // --- Spirit-Klasse ohne Partikel & Sound --- - class Spirit { - constructor(position) { - this.clock = new THREE.Clock(); - this.grp = new THREE.Group(); - this.spiritMeshes = []; - this.isFading = true; - this.grp.position.copy(position); - this.grp.position.z -= 0.6; - scene.add(this.grp); - - // Laden Spirit-GLB - const path = spiritModels[Math.floor(Math.random()*spiritModels.length)]; - gltfLoader.load(path, ({ scene: s }) => { - s.traverse(mesh => { - if(mesh.isMesh){ - mesh.castShadow = true; - mesh.receiveShadow = true; - mesh.userData.originalMaterial = mesh.material.clone(); - mesh.material = mesh.material.clone(); - mesh.material.color.set(0xffffcc); - mesh.material.opacity = 0.0; - mesh.material.emissive?.set(0xffffcc); - mesh.material.emissiveIntensity = 2.0; - this.spiritMeshes.push(mesh); - } - }); - s.rotation.x = -Math.PI; - this.grp.add(s); - }); - } - - update(dt) { - const t = this.clock.getElapsedTime(); - if(this.spiritMeshes && this.isFading){ - for(const mesh of this.spiritMeshes){ - if(t < 0.5){ - mesh.material.opacity = 1; - mesh.material.color.lerp(mesh.userData.originalMaterial.color, t/0.5); - if(mesh.material.emissive) - mesh.material.emissive.lerp(mesh.userData.originalMaterial.emissive || new THREE.Color(0x000000), t/0.5); - mesh.material.emissiveIntensity = 2.0 * (1-t/0.5) + (mesh.userData.originalMaterial.emissiveIntensity||1.0)*(t/0.5); - } else { - mesh.material.opacity = mesh.userData.originalMaterial.opacity ?? 1.0; - mesh.material.color.copy(mesh.userData.originalMaterial.color); - if(mesh.material.emissive) - mesh.material.emissive.copy(mesh.userData.originalMaterial.emissive || new THREE.Color(0x000000)); - mesh.material.emissiveIntensity = mesh.userData.originalMaterial.emissiveIntensity ?? 1.0; - this.isFading = false; - } - } + // === Spirit-Meshes aus dem Licht in Textur/Originalfarbe blenden === + if(this.spiritMeshes && this.isFading){ + for(const mesh of this.spiritMeshes){ + if(t < 0.5){ + mesh.material.opacity = 1; + mesh.material.color.lerp(mesh.userData.originalMaterial.color, t/0.5); + if(mesh.material.emissive) + mesh.material.emissive.lerp(mesh.userData.originalMaterial.emissive || new THREE.Color(0x000000), t/0.5); + mesh.material.emissiveIntensity = 2.0 * (1-t/0.5) + (mesh.userData.originalMaterial.emissiveIntensity||1.0)*(t/0.5); + } else { + mesh.material.opacity = mesh.userData.originalMaterial.opacity ?? 1.0; + mesh.material.color.copy(mesh.userData.originalMaterial.color); + if(mesh.material.emissive) + mesh.material.emissive.copy(mesh.userData.originalMaterial.emissive || new THREE.Color(0x000000)); + mesh.material.emissiveIntensity = mesh.userData.originalMaterial.emissiveIntensity ?? 1.0; + this.isFading = false; // fertig } - this.grp.position.y -= MOVE_SPEED * dt; - if (t > 15) { - scene.remove(this.grp); + } + } + + // === Bewegung nach unten === + this.grp.position.y -= MOVE_SPEED * dt; + if (t > 15) { + scene.remove(this.grp); + return false; + } + return true; + } +} + + + + + function spawnPair(){ + // Spirit + const sp = new Spirit(spinnerRed.position.clone().add(new THREE.Vector3(0,-1.5,0))); + spawned.push(sp); + // Light + /* + const pl = new THREE.PointLight(0xffe0b3,0,0); + pl.position.copy(spinnerRed.position).add(new THREE.Vector3(0,-2,0.5)); + pl.clock = new THREE.Clock(); + pl.update = function(dt){ + this.position.y -= MOVE_SPEED*dt; + if(this.clock.getElapsedTime()>20){ + scene.remove(this); return false; } return true; - } + }; + scene.add(pl); + spawned.push(pl); + */ } // --- Spinner (mit Transparenz) --- @@ -296,8 +493,10 @@ if (c.isMesh && c.material && c.material.isMeshStandardMaterial) { c.material.transparent = true; c.material.opacity = opacity; + // <- Das hier ist NEU! c.material.emissive = new THREE.Color(color); - c.material.emissiveIntensity = 3.0; + c.material.emissiveIntensity = 3.0; // experimentier ruhig mit höheren Werten! + // <- Optional: VertexColors aktivieren, falls du sie willst c.material.vertexColors = true; c.castShadow = true; } @@ -309,42 +508,53 @@ // --- Haupt-Flow --- (async()=>{ await fetchSpirits(); - landscape = await loadGLB( - 'assets/models/landscape.glb', + + // Environment (empfängt Schatten) + environment = await loadGLB( + 'assets/models/environment.glb', [0,0,0], [90,0,0], {receiveShadow:true, castShadow:true} ); + + // Tori Gate torigate = await loadGLB( - 'assets/models/tori.glb', + 'assets/models/torigate.glb', [0,6.59,0.375], [90,0,0], {receiveShadow:true, castShadow:true} ); + + // Spinner rot (z.B. models/spinner_red.glb) & Spinner blau (z.B. spinner_blue.glb) 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 ); + + // --- Low Poly Bäume als unsichtbare Schattenobjekte einbauen: shadowTree = await loadGLB( 'assets/models/tree_low.glb', [0.0,0.0,0.0], [90,0,0], {receiveShadow:false, castShadow:true, shadowOnly: true} ); + // --- Animation / Render-Loop --- function animate(){ const dt = clock.getDelta(), t = clock.getElapsedTime(); - // Spinner Animation - const bob = Math.sin(t*1.2)*0.5; + // Spinner Animation: Rotation & Schweben + const bob = Math.sin(t*1.2)*0.5; // sanftes Schweben const baseY = 16.55 + bob; spinnerRed.position.y = baseY + 0.8; spinnerBlue.position.y = baseY; - spinnerRed.rotation.y -= 1.2 * dt; - spinnerBlue.rotation.y += 1.2 * dt; + + spinnerRed.rotation.y -= 1.2 * dt; // Uhrzeigersinn + spinnerBlue.rotation.y += 1.2 * dt; // Gegen-Uhrzeigersinn // Rotierende Lichter const center = new THREE.Vector3(0, 16.55, 1.5); - const lightZ = center.z; + const lightZ = center.z; // Fix auf Z wie Spinner + // Normale Richtung (wie bisher) for(let i=0; i SPAWN_INT) { lastSpawn = t; - spawned.push(new Spirit(spinnerRed.position.clone().add(new THREE.Vector3(0,-1.5,0)))); + spawnPair(); } // update & cleanup