import React, { useEffect, useRef, useState } from "react"; import * as THREE from "three"; /* GlassIcosahedron v2 — glas-d20 med egen post-processing-pipeline. Byggd mot Three.js r128, inga externa assets, inga extra imports. Nytt i v2: - FRESNEL: MeshPhysicalMaterial patchas via onBeforeCompile så att både alpha och kantljus skalar med betraktningsvinkeln (glas är osynligt rakt framifrån, spegel i flacka vinklar). - BLOOM: egen kedja (bright-pass → separabel gaussisk blur ×2 → composite) eftersom UnrealBloomPass inte går att importera här. Bloomen sker FÖRE pixel-uppskalningen → glöden blöder ut i de feta pixlarna, CRT-style. - DISPERSION: baksidespasset är nu en egen ShaderMaterial som refraktar view-vektorn per färgkanal (olika eta för R/G/B) mot den procedurella equirect-envmappen → subtila regnbågskanter. - DITHERING: 4×4 Bayer + 5-bitars färgkvantisering i composit-passet när pixligheten ≥ 2× → äkta PS1-textur i gradienterna. - CORE GLOW: liten additiv inre ikosaeder → "ljus fångat i glaset". - Vinjett + manuell linear→sRGB i composite (RT:erna hålls linjära). */ const BG_PRESETS = ["#05060a", "#0b1020", "#1a0b1e", "#001510"]; // ---------- Shaders för post-kedjan ---------- const FS_VERT = /* glsl */ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); } `; const BRIGHT_FRAG = /* glsl */ ` uniform sampler2D tInput; uniform float uThreshold; uniform float uKnee; varying vec2 vUv; void main() { vec3 c = texture2D(tInput, vUv).rgb; float l = dot(c, vec3(0.2126, 0.7152, 0.0722)); float w = smoothstep(uThreshold, uThreshold + uKnee, l); gl_FragColor = vec4(c * w, 1.0); } `; const BLUR_FRAG = /* glsl */ ` uniform sampler2D tInput; uniform vec2 uDir; // (texel,0) eller (0,texel) varying vec2 vUv; void main() { // 9-tap gauss vec3 acc = texture2D(tInput, vUv).rgb * 0.227027; vec2 o1 = uDir * 1.3846153846; vec2 o2 = uDir * 3.2307692308; acc += texture2D(tInput, vUv + o1).rgb * 0.3162162162; acc += texture2D(tInput, vUv - o1).rgb * 0.3162162162; acc += texture2D(tInput, vUv + o2).rgb * 0.0702702703; acc += texture2D(tInput, vUv - o2).rgb * 0.0702702703; gl_FragColor = vec4(acc, 1.0); } `; const COMPOSITE_FRAG = /* glsl */ ` uniform sampler2D tScene; uniform sampler2D tBloom; uniform float uBloom; uniform float uVignette; uniform float uQuantLevels; // 0 = av varying vec2 vUv; // Ordered dithering: rekursiv Bayer, värden ~[0,1) float bayer2(vec2 a) { a = floor(a); return fract(a.x * 0.5 + a.y * a.y * 0.75); } float bayer4(vec2 a) { return bayer2(0.5 * a) * 0.25 + bayer2(a); } void main() { vec3 col = texture2D(tScene, vUv).rgb; col += texture2D(tBloom, vUv).rgb * uBloom; // Vinjett float d = distance(vUv, vec2(0.5)); col *= 1.0 - uVignette * smoothstep(0.35, 0.9, d); // Linear → sRGB (RT:erna är linjära) col = pow(max(col, vec3(0.0)), vec3(1.0 / 2.2)); // Bayer-dithering + färgkvantisering (PS1-läge) if (uQuantLevels > 0.5) { float b = bayer4(gl_FragCoord.xy) - 0.5; float n = uQuantLevels - 1.0; col = floor(col * n + 0.5 + b) / n; } gl_FragColor = vec4(col, 1.0); } `; // Baksidespass: dispersion — refrakta per kanal mot equirect-envmappen const DISPERSION_VERT = /* glsl */ ` varying vec3 vWorldPos; varying vec3 vWorldNormal; void main() { vec4 wp = modelMatrix * vec4(position, 1.0); vWorldPos = wp.xyz; vWorldNormal = normalize(mat3(modelMatrix) * normal); gl_Position = projectionMatrix * viewMatrix * wp; } `; const DISPERSION_FRAG = /* glsl */ ` uniform sampler2D tEnv; uniform vec3 uTint; uniform float uGain; uniform float uThickness; // 0..1 → brytning, dispersion, absorption uniform float uAlphaMul; // styrs av transparens-slidern varying vec3 vWorldPos; varying vec3 vWorldNormal; vec2 equirectUv(vec3 dir) { float u = atan(dir.z, dir.x) * 0.15915494309 + 0.5; // 1/(2*pi) float v = asin(clamp(dir.y, -1.0, 1.0)) * 0.31830988618 + 0.5; // 1/pi return vec2(u, v); } void main() { vec3 V = normalize(vWorldPos - cameraPosition); // BackSide → geometrinormalen pekar bort från kameran; vänd den vec3 N = -normalize(vWorldNormal); // Tjockleken styr hur mycket strålen böjs och hur brett kanalerna sprids float bend = mix(0.03, 0.42, uThickness); float spread = 0.012 + 0.07 * uThickness; float etaG = 1.0 - bend; vec3 rr = refract(V, N, etaG + spread); vec3 rg = refract(V, N, etaG); vec3 rb = refract(V, N, etaG - spread); // refract kan ge nollvektor vid totalreflektion → fall tillbaka på reflect if (dot(rr, rr) < 0.001) rr = reflect(V, N); if (dot(rg, rg) < 0.001) rg = reflect(V, N); if (dot(rb, rb) < 0.001) rb = reflect(V, N); // Tjockare glas → frostigare inre (högre mip-bias) float mip = 1.0 + 3.0 * uThickness; vec3 env; env.r = texture2D(tEnv, equirectUv(rr), mip).r; env.g = texture2D(tEnv, equirectUv(rg), mip).g; env.b = texture2D(tEnv, equirectUv(rb), mip).b; float fres = pow(1.0 - abs(dot(V, N)), 2.0); vec3 col = env * uTint * uGain * (0.35 + 0.65 * fres); // Beer-Lambert: tjockare glas absorberar mer av komplementfärgen vec3 absorb = vec3(1.0) - uTint; col *= exp(-absorb * uThickness * 2.5); // Fresnel-viktad alpha × transparens: rakt igenom mitten släpps bakgrunden fram float alpha = (0.06 + 0.6 * fres) * uAlphaMul; gl_FragColor = vec4(col, alpha); } `; // Inre glöd — "ljus fångat i glaset" const CORE_VERT = /* glsl */ ` varying vec3 vN; varying vec3 vV; void main() { vN = normalize(normalMatrix * normal); vec4 mv = modelViewMatrix * vec4(position, 1.0); vV = normalize(-mv.xyz); gl_Position = projectionMatrix * mv; } `; const CORE_FRAG = /* glsl */ ` uniform vec3 uTint; uniform float uGain; varying vec3 vN; varying vec3 vV; void main() { float f = pow(clamp(dot(normalize(vN), normalize(vV)), 0.0, 1.0), 2.5); gl_FragColor = vec4(uTint * f * uGain, f); } `; export default function GlassIcosahedron() { const containerRef = useRef(null); const [bg, setBg] = useState("#05060a"); const [pixel, setPixel] = useState(1); const [speed, setSpeed] = useState(0.5); const [glow, setGlow] = useState(0.8); const [glass, setGlass] = useState("#7fd4ff"); const [transp, setTransp] = useState(0.75); // 1 = mest genomskinligt const [thick, setThick] = useState(0.45); // 0 = tunt, 1 = massivt const [panelOpen, setPanelOpen] = useState(true); const bgRef = useRef(bg); const pixelRef = useRef(pixel); const speedRef = useRef(speed); const glowRef = useRef(glow); const glassRef = useRef(glass); const transpRef = useRef(transp); const thickRef = useRef(thick); const threeRef = useRef(null); const resizeRef = useRef(null); bgRef.current = bg; speedRef.current = speed; glowRef.current = glow; transpRef.current = transp; thickRef.current = thick; useEffect(() => { const container = containerRef.current; if (!container) return; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100); camera.position.set(0, 0, 5.2); const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: false, powerPreference: "high-performance", }); // RT:erna hålls linjära; sRGB görs manuellt i composite-shadern renderer.outputEncoding = THREE.LinearEncoding; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.15; renderer.domElement.style.width = "100%"; renderer.domElement.style.height = "100%"; renderer.domElement.style.display = "block"; renderer.domElement.style.imageRendering = "pixelated"; renderer.domElement.style.touchAction = "none"; container.appendChild(renderer.domElement); // ---------- Procedurell envmap: krispigare strips → hårdare speculars ---------- const envCanvas = document.createElement("canvas"); envCanvas.width = 1024; envCanvas.height = 512; const ectx = envCanvas.getContext("2d"); const base = ectx.createLinearGradient(0, 0, 0, 512); base.addColorStop(0, "#161c2a"); base.addColorStop(0.45, "#04060a"); base.addColorStop(1, "#010102"); ectx.fillStyle = base; ectx.fillRect(0, 0, 1024, 512); const blob = (x, y, r, color, alpha) => { const g = ectx.createRadialGradient(x, y, 0, x, y, r); g.addColorStop(0, color); g.addColorStop(1, "rgba(0,0,0,0)"); ectx.globalAlpha = alpha; ectx.fillStyle = g; ectx.fillRect(x - r, y - r, r * 2, r * 2); ectx.globalAlpha = 1; }; // Skarpa, små ljuskällor → tydliga glimtar i reflektionen blob(200, 110, 60, "#ffffff", 1.0); blob(200, 110, 160, "#fff2d8", 0.5); blob(770, 90, 45, "#eaf6ff", 1.0); blob(770, 90, 130, "#bfe8ff", 0.45); blob(520, 340, 220, "#33224d", 0.5); // Tunna hårda softbox-strips (det är dessa som ger "dyra" reflektioner) const strip = (x, y, w, h, a) => { const g = ectx.createLinearGradient(0, y, 0, y + h); g.addColorStop(0, "rgba(255,255,255,0)"); g.addColorStop(0.5, `rgba(255,255,255,${a})`); g.addColorStop(1, "rgba(255,255,255,0)"); ectx.fillStyle = g; ectx.fillRect(x, y, w, h); }; strip(280, 150, 560, 22, 0.9); strip(60, 240, 300, 10, 0.55); strip(700, 260, 260, 8, 0.5); const envTex = new THREE.CanvasTexture(envCanvas); envTex.mapping = THREE.EquirectangularReflectionMapping; envTex.minFilter = THREE.LinearMipmapLinearFilter; envTex.generateMipmaps = true; const pmrem = new THREE.PMREMGenerator(renderer); pmrem.compileEquirectangularShader(); const envRT = pmrem.fromEquirectangular(envTex); scene.environment = envRT.texture; // ---------- Geometri ---------- const group = new THREE.Group(); scene.add(group); const geo = new THREE.IcosahedronGeometry(1.35, 0); const edgesGeo = new THREE.EdgesGeometry(geo); const linColor = (hex) => new THREE.Color(hex).convertSRGBToLinear(); // 1) Depth-prepass (osynlig) → maskar de bakre kanterna korrekt const depthMat = new THREE.MeshBasicMaterial({ colorWrite: false, depthWrite: true, }); const depthMesh = new THREE.Mesh(geo, depthMat); depthMesh.renderOrder = 0; group.add(depthMesh); // 2) Bakre kanter: streckade, syns bara genom glaset const backEdgeMat = new THREE.LineDashedMaterial({ color: linColor(glassRef.current), dashSize: 0.09, gapSize: 0.06, transparent: true, opacity: 0.3, depthFunc: THREE.GreaterDepth, depthWrite: false, }); const backEdges = new THREE.LineSegments(edgesGeo, backEdgeMat); backEdges.computeLineDistances(); backEdges.renderOrder = 1; group.add(backEdges); // 3) Core glow — inre ljuskälla const coreMat = new THREE.ShaderMaterial({ vertexShader: CORE_VERT, fragmentShader: CORE_FRAG, uniforms: { uTint: { value: linColor(glassRef.current) }, uGain: { value: 0.5 }, }, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, depthTest: false, }); const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.72, 1), coreMat); core.renderOrder = 2; group.add(core); // 4) Baksida: dispersion-glas (egen shader, refraktion per kanal) const dispMat = new THREE.ShaderMaterial({ vertexShader: DISPERSION_VERT, fragmentShader: DISPERSION_FRAG, uniforms: { tEnv: { value: envTex }, uTint: { value: linColor(glassRef.current) }, uGain: { value: 1.2 }, uThickness: { value: thickRef.current }, uAlphaMul: { value: 1.0 }, }, transparent: true, side: THREE.BackSide, depthWrite: false, depthTest: false, // depth-bufferten har framsidan; rita innerväggen ändå }); const glassBack = new THREE.Mesh(geo, dispMat); glassBack.renderOrder = 3; group.add(glassBack); // 5) Framsida: MeshPhysicalMaterial med fresnel-patch const frontMat = new THREE.MeshPhysicalMaterial({ color: linColor(glassRef.current), metalness: 0, roughness: 0.06, transparent: true, opacity: 1, // alfa styrs av fresnel-patchen envMapIntensity: 1.6, clearcoat: 1, clearcoatRoughness: 0.08, side: THREE.FrontSide, depthWrite: false, premultipliedAlpha: true, }); frontMat.onBeforeCompile = (shader) => { shader.uniforms.uAlphaBase = { value: 0.06 }; shader.uniforms.uFresGain = { value: 0.9 }; shader.uniforms.uEdgeTint = { value: linColor(glassRef.current) }; shader.uniforms.uThickness = { value: thickRef.current }; shader.uniforms.uTint = { value: linColor(glassRef.current) }; shader.fragmentShader = shader.fragmentShader.replace( "void main() {", `uniform float uAlphaBase; uniform float uFresGain; uniform vec3 uEdgeTint; uniform float uThickness; uniform vec3 uTint; void main() {` ); // r128 har INTE chunken (kom i r137) — slutraden // ligger som literal kod. Hantera båda varianterna för säkerhets skull. const patch = ` float fresF = pow(1.0 - clamp(dot(normalize(vViewPosition), normal), 0.0, 1.0), 3.0); outgoingLight += uEdgeTint * fresF * uFresGain; // Beer-Lambert: tjockare glas absorberar komplementfärgen → djupare mättnad vec3 absorbF = (vec3(1.0) - uTint) * uThickness * 3.0; outgoingLight *= exp(-absorbF); // Tjockt glas läser som tätare i mitten → högre bas-alpha float baseA = mix(uAlphaBase, uAlphaBase + 0.35, uThickness); float alphaF = mix(baseA, 1.0, fresF); gl_FragColor = vec4(outgoingLight, alphaF);`; const literal = "gl_FragColor = vec4( outgoingLight, diffuseColor.a );"; if (shader.fragmentShader.includes(literal)) { shader.fragmentShader = shader.fragmentShader.replace(literal, patch); } else if (shader.fragmentShader.includes("#include ")) { shader.fragmentShader = shader.fragmentShader.replace( "#include ", patch ); } else { console.warn("GlassIcosahedron: hittade ingen output-rad att patcha"); } frontMat.userData.shader = shader; }; const glassFront = new THREE.Mesh(geo, frontMat); glassFront.renderOrder = 4; group.add(glassFront); // 6) Främre kanter (matas rakt in i bloomen) const edgeColor = linColor(glassRef.current).lerp( new THREE.Color(1, 1, 1), 0.55 ); const frontEdgeMat = new THREE.LineBasicMaterial({ color: edgeColor.clone(), transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false, }); const frontEdges = new THREE.LineSegments(edgesGeo, frontEdgeMat); frontEdges.scale.setScalar(1.002); frontEdges.renderOrder = 5; group.add(frontEdges); // 7) Hörnglimtar const sparkCanvas = document.createElement("canvas"); sparkCanvas.width = sparkCanvas.height = 64; const sctx = sparkCanvas.getContext("2d"); const sg = sctx.createRadialGradient(32, 32, 0, 32, 32, 32); sg.addColorStop(0, "rgba(255,255,255,1)"); sg.addColorStop(0.25, "rgba(255,255,255,0.7)"); sg.addColorStop(1, "rgba(255,255,255,0)"); sctx.fillStyle = sg; sctx.fillRect(0, 0, 64, 64); const sparkTex = new THREE.CanvasTexture(sparkCanvas); const posAttr = geo.attributes.position; const seen = new Set(); const verts = []; for (let i = 0; i < posAttr.count; i++) { const x = posAttr.getX(i), y = posAttr.getY(i), z = posAttr.getZ(i); const key = `${x.toFixed(3)},${y.toFixed(3)},${z.toFixed(3)}`; if (!seen.has(key)) { seen.add(key); verts.push(x, y, z); } } const sparkGeo = new THREE.BufferGeometry(); sparkGeo.setAttribute("position", new THREE.Float32BufferAttribute(verts, 3)); const sparkMat = new THREE.PointsMaterial({ map: sparkTex, color: edgeColor.clone(), size: 0.16, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true, }); const sparks = new THREE.Points(sparkGeo, sparkMat); sparks.scale.setScalar(1.004); sparks.renderOrder = 6; group.add(sparks); // ---------- Ljus ---------- const keyLight = new THREE.DirectionalLight(0xfff1d6, 1.3); keyLight.position.set(3, 4, 2); scene.add(keyLight); const rimLight = new THREE.PointLight(0x66ccff, 1.6, 30); rimLight.position.set(-4, 2, -3); scene.add(rimLight); const fillLight = new THREE.PointLight(0xff6fb0, 0.7, 30); fillLight.position.set(2.5, -3, 4); scene.add(fillLight); scene.add(new THREE.AmbientLight(0x1a2233, 0.6)); // ---------- Post-processing: fullscreen-quad + RT-kedja ---------- const fsScene = new THREE.Scene(); const fsCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const fsGeo = new THREE.PlaneBufferGeometry(2, 2); const fsQuad = new THREE.Mesh(fsGeo, null); fsQuad.frustumCulled = false; fsScene.add(fsQuad); const brightMat = new THREE.ShaderMaterial({ vertexShader: FS_VERT, fragmentShader: BRIGHT_FRAG, uniforms: { tInput: { value: null }, uThreshold: { value: 0.55 }, uKnee: { value: 0.25 }, }, depthTest: false, depthWrite: false, }); const blurMat = new THREE.ShaderMaterial({ vertexShader: FS_VERT, fragmentShader: BLUR_FRAG, uniforms: { tInput: { value: null }, uDir: { value: new THREE.Vector2() }, }, depthTest: false, depthWrite: false, }); const compositeMat = new THREE.ShaderMaterial({ vertexShader: FS_VERT, fragmentShader: COMPOSITE_FRAG, uniforms: { tScene: { value: null }, tBloom: { value: null }, uBloom: { value: 1.0 }, uVignette: { value: 0.35 }, uQuantLevels: { value: 0.0 }, }, depthTest: false, depthWrite: false, }); const rts = { scene: null, a: null, b: null }; const disposeRTs = () => { if (rts.scene) rts.scene.dispose(); if (rts.a) rts.a.dispose(); if (rts.b) rts.b.dispose(); rts.scene = rts.a = rts.b = null; }; const buildRTs = (iw, ih) => { disposeRTs(); rts.scene = new THREE.WebGLRenderTarget(iw, ih, { minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, format: THREE.RGBAFormat, depthBuffer: true, stencilBuffer: false, }); const bw = Math.max(2, iw >> 1); const bh = Math.max(2, ih >> 1); const opts = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, depthBuffer: false, stencilBuffer: false, }; rts.a = new THREE.WebGLRenderTarget(bw, bh, opts); rts.b = new THREE.WebGLRenderTarget(bw, bh, opts); }; // ---------- Drag med inertia (pointer events) ---------- const drag = { active: false, lastX: 0, lastY: 0, lastT: 0, velX: 0, velY: speedRef.current, }; const DRAG_SENS = 0.007; const canvas = renderer.domElement; const onPointerDown = (e) => { drag.active = true; drag.lastX = e.clientX; drag.lastY = e.clientY; drag.lastT = performance.now(); canvas.setPointerCapture(e.pointerId); canvas.style.cursor = "grabbing"; }; const onPointerMove = (e) => { if (!drag.active) return; const now = performance.now(); const dt = Math.max((now - drag.lastT) / 1000, 1 / 240); const dx = e.clientX - drag.lastX; const dy = e.clientY - drag.lastY; rotateWorld(group, dy * DRAG_SENS, dx * DRAG_SENS); drag.velY = (dx * DRAG_SENS) / dt; drag.velX = (dy * DRAG_SENS) / dt; drag.lastX = e.clientX; drag.lastY = e.clientY; drag.lastT = now; }; const onPointerUp = (e) => { drag.active = false; try { canvas.releasePointerCapture(e.pointerId); } catch (_) {} canvas.style.cursor = "grab"; }; canvas.style.cursor = "grab"; canvas.addEventListener("pointerdown", onPointerDown); window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointerup", onPointerUp); window.addEventListener("pointercancel", onPointerUp); const qTmp = new THREE.Quaternion(); const X_AXIS = new THREE.Vector3(1, 0, 0); const Y_AXIS = new THREE.Vector3(0, 1, 0); function rotateWorld(obj, angleX, angleY) { if (angleY) { qTmp.setFromAxisAngle(Y_AXIS, angleY); obj.quaternion.premultiply(qTmp); } if (angleX) { qTmp.setFromAxisAngle(X_AXIS, angleX); obj.quaternion.premultiply(qTmp); } } // ---------- Storlek + pixlighet ---------- const applySize = () => { const w = container.clientWidth || 1; const h = container.clientHeight || 1; const p = Math.max(1, pixelRef.current); const dpr = p <= 1 ? Math.min(window.devicePixelRatio || 1, 2) : 1; const iw = Math.max(2, Math.floor((w * dpr) / p)); const ih = Math.max(2, Math.floor((h * dpr) / p)); renderer.setPixelRatio(1); renderer.setSize(iw, ih, false); camera.aspect = w / h; camera.updateProjectionMatrix(); buildRTs(iw, ih); }; resizeRef.current = applySize; applySize(); const ro = new ResizeObserver(applySize); ro.observe(container); // ---------- Render-loop ---------- let raf = 0; let prevT = performance.now(); const bgColor = new THREE.Color(); const tick = () => { raf = requestAnimationFrame(tick); const now = performance.now(); const dt = Math.min((now - prevT) / 1000, 0.05); prevT = now; if (!drag.active) { const tau = 1.4; const k = 1 - Math.exp(-dt / tau); drag.velY += (speedRef.current - drag.velY) * k; drag.velX += (0 - drag.velX) * k; rotateWorld(group, drag.velX * dt, drag.velY * dt); } sparkMat.size = 0.14 + Math.sin(now * 0.004) * 0.025; const g = glowRef.current; const tr = transpRef.current; const th = thickRef.current; frontEdgeMat.opacity = 0.25 + 0.75 * g; sparkMat.opacity = 0.2 + 0.8 * g; // Tjockare glas → bakre kanter syns svagare genom massan backEdgeMat.opacity = (0.08 + 0.32 * g) * (1.0 - 0.55 * th); // Core-glowen dämpas när glaset är mer genomskinligt coreMat.uniforms.uGain.value = (0.15 + 0.6 * g) * (1.1 - 0.6 * tr); compositeMat.uniforms.uBloom.value = 0.3 + 1.5 * g; // Transparens: styr framsidans bas-alpha och baksidans genomsläpp const fsh = frontMat.userData.shader; if (fsh) { fsh.uniforms.uAlphaBase.value = 0.02 + (1.0 - tr) * 0.72; fsh.uniforms.uThickness.value = th; } // Tjockare glas → baksidespasset (brytning/absorption) syns starkare dispMat.uniforms.uAlphaMul.value = (0.35 + (1.0 - tr) * 0.95) * (0.7 + 0.9 * th); dispMat.uniforms.uThickness.value = th; bgColor.set(bgRef.current).convertSRGBToLinear(); scene.background = scene.background || new THREE.Color(); scene.background.copy(bgColor); const p = pixelRef.current; compositeMat.uniforms.uQuantLevels.value = p >= 2 ? 32 : 0; // PASS 1: scenen → lågupplöst RT (linjär, tonemappad) renderer.setRenderTarget(rts.scene); renderer.render(scene, camera); // PASS 2: bright-pass → halvupplöst fsQuad.material = brightMat; brightMat.uniforms.tInput.value = rts.scene.texture; renderer.setRenderTarget(rts.a); renderer.render(fsScene, fsCam); // PASS 3–6: separabel gaussisk blur, 2 iterationer const tx = 1 / rts.a.width; const ty = 1 / rts.a.height; fsQuad.material = blurMat; for (let i = 0; i < 2; i++) { blurMat.uniforms.tInput.value = rts.a.texture; blurMat.uniforms.uDir.value.set(tx, 0); renderer.setRenderTarget(rts.b); renderer.render(fsScene, fsCam); blurMat.uniforms.tInput.value = rts.b.texture; blurMat.uniforms.uDir.value.set(0, ty); renderer.setRenderTarget(rts.a); renderer.render(fsScene, fsCam); } // PASS 7: composite (bloom + vinjett + sRGB + dither) → skärmen fsQuad.material = compositeMat; compositeMat.uniforms.tScene.value = rts.scene.texture; compositeMat.uniforms.tBloom.value = rts.a.texture; renderer.setRenderTarget(null); renderer.render(fsScene, fsCam); }; tick(); threeRef.current = { frontMat, dispMat, coreMat, backEdgeMat, frontEdgeMat, sparkMat, }; // ---------- Städning ---------- return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener("pointerdown", onPointerDown); window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointerup", onPointerUp); window.removeEventListener("pointercancel", onPointerUp); geo.dispose(); edgesGeo.dispose(); sparkGeo.dispose(); core.geometry.dispose(); fsGeo.dispose(); depthMat.dispose(); backEdgeMat.dispose(); coreMat.dispose(); dispMat.dispose(); frontMat.dispose(); frontEdgeMat.dispose(); sparkMat.dispose(); brightMat.dispose(); blurMat.dispose(); compositeMat.dispose(); sparkTex.dispose(); envTex.dispose(); envRT.dispose(); pmrem.dispose(); disposeRTs(); renderer.dispose(); if (renderer.domElement.parentNode === container) { container.removeChild(renderer.domElement); } threeRef.current = null; }; }, []); // ---------- Reaktiva uppdateringar ---------- useEffect(() => { pixelRef.current = pixel; if (resizeRef.current) resizeRef.current(); }, [pixel]); useEffect(() => { glassRef.current = glass; const t = threeRef.current; if (!t) return; const c = new THREE.Color(glass).convertSRGBToLinear(); t.frontMat.color.copy(c); if (t.frontMat.userData.shader) { t.frontMat.userData.shader.uniforms.uEdgeTint.value.copy(c); t.frontMat.userData.shader.uniforms.uTint.value.copy(c); } t.dispMat.uniforms.uTint.value.copy(c); t.coreMat.uniforms.uTint.value.copy(c); t.backEdgeMat.color.copy(c); const edge = c.clone().lerp(new THREE.Color(1, 1, 1), 0.55); t.frontEdgeMat.color.copy(edge); t.sparkMat.color.copy(edge); }, [glass]); // ---------- UI ---------- const S = styles; return (
{panelOpen && (
)}
); } const styles = { root: { position: "relative", width: "100%", height: "100%", minHeight: 320, overflow: "hidden", background: "#000", }, panel: { position: "absolute", top: 10, left: 10, zIndex: 2, fontFamily: "ui-monospace, 'Cascadia Mono', 'JetBrains Mono', monospace", fontSize: 11, color: "#cfd8e3", userSelect: "none", }, toggle: { background: "rgba(10,14,20,0.75)", border: "1px solid #2a3140", color: "#cfd8e3", borderRadius: 4, padding: "4px 8px", cursor: "pointer", fontFamily: "inherit", fontSize: 11, letterSpacing: "0.05em", }, controls: { marginTop: 6, background: "rgba(10,14,20,0.75)", border: "1px solid #2a3140", borderRadius: 6, padding: "10px 12px", display: "flex", flexDirection: "column", gap: 10, backdropFilter: "blur(6px)", WebkitBackdropFilter: "blur(6px)", minWidth: 210, }, row: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, }, rowRight: { display: "flex", alignItems: "center", gap: 5 }, label: { textTransform: "uppercase", letterSpacing: "0.08em", opacity: 0.8, whiteSpace: "nowrap", }, slider: { width: 110, accentColor: "#7fd4ff" }, swatch: { width: 16, height: 16, borderRadius: 3, border: "none", cursor: "pointer", padding: 0, }, color: { width: 26, height: 20, border: "1px solid #2a3140", borderRadius: 3, background: "transparent", padding: 0, cursor: "pointer", }, };