auto-git:

[change] index.html
This commit is contained in:
2025-05-28 00:54:44 +02:00
parent fbafd3dbdc
commit e1ac5a8ec7

View File

@@ -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 { 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.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(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,8 +266,9 @@
const rotatingLights = [];
const counterRotatingLights = [];
const LIGHT_RADIUS = 1;
for(let i=0;i<3;i++){
const L = new THREE.PointLight(0xFFA230, 5, 30);
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);
@@ -219,19 +277,69 @@
L2.castShadow = true;
counterRotatingLights.push(L2);
scene.add(L2);
}
}
// --- Spirit-Klasse ohne Partikel & Sound ---
class Spirit {
// 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;
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 }) => {
@@ -239,11 +347,14 @@
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);
mesh.material.emissive?.set(0xffffcc); // Falls vorhanden
mesh.material.emissiveIntensity = 2.0;
this.spiritMeshes.push(mesh);
}
@@ -253,8 +364,67 @@
});
}
// === 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){
@@ -269,10 +439,12 @@
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;
this.isFading = false; // fertig
}
}
}
// === Bewegung nach unten ===
this.grp.position.y -= MOVE_SPEED * dt;
if (t > 15) {
scene.remove(this.grp);
@@ -280,6 +452,31 @@
}
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<rotatingLights.length; i++){
const ang = t * 0.8 + i * 2 * Math.PI / 3;
rotatingLights[i].position.set(
@@ -353,6 +563,8 @@
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(
@@ -361,11 +573,10 @@
lightZ
);
}
// Spawn Spirits
// Spawn Spirits/Lichter
if (t - lastSpawn > SPAWN_INT) {
lastSpawn = t;
spawned.push(new Spirit(spinnerRed.position.clone().add(new THREE.Vector3(0,-1.5,0))));
spawnPair();
}
// update & cleanup