595 lines
21 KiB
HTML
595 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>Three.js Spirits Szene</title>
|
||
<script type="importmap">
|
||
{
|
||
"imports": {
|
||
"three": "https://cdn.jsdelivr.net/npm/three@0.155.0/build/three.module.js",
|
||
"GLTFLoader": "https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/loaders/GLTFLoader.js",
|
||
"DRACOLoader": "https://cdn.jsdelivr.net/npm/three@0.155.0/examples/jsm/loaders/DRACOLoader.js"
|
||
}
|
||
}
|
||
</script>
|
||
<style>
|
||
#viewer {
|
||
position: absolute; top:50%; left:50%;
|
||
transform: translate(-50%,-50%);
|
||
width:1200px; height:800px;
|
||
max-width:100%; max-height:100%;
|
||
background:#000; border-radius:15px; overflow:hidden;
|
||
}
|
||
canvas { display:block; }
|
||
body { margin:0; overflow:hidden; background:#000; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="viewer"></div>
|
||
<script type="module">
|
||
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'; // 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();
|
||
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 } // 0 = aus, 1 = voll
|
||
},
|
||
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);
|
||
// Normales Overlay: Foliage-Alpha mischt drüber
|
||
gl_FragColor = mix(base, vec4(foliage.rgb, base.a), foliage.a * opacity);
|
||
}
|
||
`
|
||
};
|
||
|
||
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(ssaoPass);
|
||
composer.addPass(bloomPass);
|
||
//composer.addPass(brightnessContrastPass);
|
||
composer.addPass(vignettePass);
|
||
composer.addPass(rgbShiftPass);
|
||
//composer.addPass(filmPass);
|
||
composer.addPass(gammaPass);
|
||
|
||
|
||
// end of postprocessing
|
||
|
||
|
||
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; // 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);
|
||
camera.updateProjectionMatrix();
|
||
}
|
||
window.addEventListener('resize',onResize);
|
||
onResize();
|
||
|
||
// --- HDRI Environment ---
|
||
const texLoader = new THREE.TextureLoader();
|
||
const pmremGen = new THREE.PMREMGenerator(renderer);
|
||
texLoader.load('assets/hdri/environment.png', tex => {
|
||
const envRT = pmremGen.fromEquirectangular(tex).texture;
|
||
scene.environment = envRT;
|
||
scene.background = envRT;
|
||
tex.dispose();
|
||
pmremGen.dispose();
|
||
});
|
||
|
||
// --- 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;
|
||
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);
|
||
|
||
// --- Spirits automatisch einsammeln ---
|
||
let spiritModels = [];
|
||
async function fetchSpirits(){
|
||
const res = await fetch('assets/models/spirits/');
|
||
const html = await res.text();
|
||
const doc = new DOMParser().parseFromString(html,'text/html');
|
||
spiritModels = [...doc.querySelectorAll('a')]
|
||
.map(a => a.getAttribute('href'))
|
||
.filter(h=>h.endsWith('.glb'))
|
||
.map(name=>`assets/models/spirits/${name}`);
|
||
}
|
||
|
||
// --- 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]);
|
||
obj.rotation.set(
|
||
THREE.MathUtils.degToRad(rotDeg[0]),
|
||
THREE.MathUtils.degToRad(rotDeg[1]),
|
||
THREE.MathUtils.degToRad(rotDeg[2])
|
||
);
|
||
obj.traverse(c => {
|
||
c.visible = visible; // MUSS true sein, sonst kein Schatten!
|
||
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;
|
||
}
|
||
|
||
// --- Variablen für Animation ---
|
||
let spinnerRed, spinnerBlue, torigate, environment, shadowTree;
|
||
const clock = new THREE.Clock();
|
||
let lastSpawn = 0;
|
||
const SPAWN_INT = 5;
|
||
const MOVE_SPEED= 1;
|
||
const spawned = [];
|
||
|
||
// --- Rotierende Lichter um Spinner (nur um Y!) ---
|
||
const rotatingLights = [];
|
||
const counterRotatingLights = [];
|
||
const LIGHT_RADIUS = 1;
|
||
|
||
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<N; i++){
|
||
// Punkte zufällig in Halb-Kugel, random Richtung nach oben
|
||
const theta = Math.random()*2*Math.PI;
|
||
const phi = Math.random()*Math.PI/1.7;
|
||
const r = Math.random()*0.2+0.5;
|
||
const x = Math.sin(phi)*Math.cos(theta)*r;
|
||
const y = Math.abs(Math.cos(phi))*r; // <-- Y ist nach oben
|
||
const z = Math.sin(phi)*Math.sin(theta)*r;
|
||
positions.push(x, y, z);
|
||
velocities.push(x*2, y*2.5+0.7, z*2);
|
||
}
|
||
geom.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||
geom.setAttribute('velocity', new THREE.Float32BufferAttribute(velocities, 3));
|
||
const mat = new THREE.PointsMaterial({
|
||
size: 1.3,
|
||
color: 0xffffff,
|
||
map: particleTexture,
|
||
blending: THREE.AdditiveBlending,
|
||
alphaTest: 0.15,
|
||
depthWrite: false,
|
||
transparent: true,
|
||
opacity: 1.0
|
||
});
|
||
const pts = new THREE.Points(geom, mat);
|
||
pts.userData.lifetime = 0.7 + Math.random()*0.13;
|
||
pts.userData.age = 0;
|
||
return pts;
|
||
}
|
||
|
||
update(dt) {
|
||
const t = this.clock.getElapsedTime();
|
||
|
||
// === Partikel animieren und ggf. entfernen ===
|
||
if(this.particles){
|
||
this.particles.userData.age += dt;
|
||
const pos = this.particles.geometry.attributes.position;
|
||
const vel = this.particles.geometry.attributes.velocity;
|
||
for(let i=0; i<pos.count; i++){
|
||
// Linear nach außen
|
||
pos.array[3*i+0] += vel.array[3*i+0]*dt;
|
||
pos.array[3*i+1] += vel.array[3*i+1]*dt;
|
||
pos.array[3*i+2] += vel.array[3*i+2]*dt;
|
||
}
|
||
pos.needsUpdate = true;
|
||
// Transparenz schnell ausfaden
|
||
this.particles.material.opacity = 0.7 * (1.0 - this.particles.userData.age/this.particles.userData.lifetime);
|
||
if(this.particles.userData.age > this.particles.userData.lifetime){
|
||
this.grp.remove(this.particles);
|
||
this.particles.geometry.dispose();
|
||
this.particles.material.dispose();
|
||
this.particles = null;
|
||
}
|
||
}
|
||
|
||
// === 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
|
||
}
|
||
}
|
||
}
|
||
|
||
// === 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) ---
|
||
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;
|
||
// <- Das hier ist NEU!
|
||
c.material.emissive = new THREE.Color(color);
|
||
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;
|
||
}
|
||
});
|
||
scene.add(obj);
|
||
return obj;
|
||
}
|
||
|
||
// --- Haupt-Flow ---
|
||
(async()=>{
|
||
await fetchSpirits();
|
||
|
||
// 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/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: 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; // 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; // Fix auf Z wie Spinner
|
||
// Normale Richtung (wie bisher)
|
||
for(let i=0; i<rotatingLights.length; i++){
|
||
const ang = t * 0.8 + i * 2 * Math.PI / 3;
|
||
rotatingLights[i].position.set(
|
||
center.x + Math.cos(ang) * LIGHT_RADIUS,
|
||
center.y + Math.sin(ang) * LIGHT_RADIUS,
|
||
lightZ
|
||
);
|
||
}
|
||
|
||
// Gegendrehende Lichter:
|
||
for(let i=0; i<counterRotatingLights.length; i++){
|
||
const ang = -t * 0.8 + i * 2 * Math.PI / 3;
|
||
counterRotatingLights[i].position.set(
|
||
center.x + Math.cos(ang) * LIGHT_RADIUS,
|
||
center.y + Math.sin(ang) * LIGHT_RADIUS,
|
||
lightZ
|
||
);
|
||
}
|
||
// Spawn Spirits/Lichter
|
||
if (t - lastSpawn > SPAWN_INT) {
|
||
lastSpawn = t;
|
||
spawnPair();
|
||
}
|
||
|
||
// update & cleanup
|
||
for (let i = spawned.length-1; i >= 0; i--) {
|
||
if (!spawned[i].update(dt)) spawned.splice(i,1);
|
||
}
|
||
|
||
composer.render(scene, camera);
|
||
requestAnimationFrame(animate);
|
||
}
|
||
animate();
|
||
})();
|
||
|
||
</script>
|
||
</body>
|
||
</html> |