Files
virtual_interdimensional_sp…/index.html
Victor Giers e1ac5a8ec7 auto-git:
[change] index.html
2025-05-28 00:54:44 +02:00

595 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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.10.3 sieht am realistischsten aus)
ssaoPass.output = SSAOPass.OUTPUT.Default; // Normal/Default
ssaoPass.bias = 0.05; // "Füllung" (0.010.1)
ssaoPass.aoClamp = 0.5; // Kontrast/Abdunklung (0.11.0)
ssaoPass.lumInfluence = 1; // Wie stark das Umgebungslicht AO beeinflusst
ssaoPass.intensity = 1.7; // Hauptstärke des Effekts (1.03.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.0010.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>