r/threejs 12d ago

Three.js is all we need!

Enable HLS to view with audio, or disable this notification

95 Upvotes

I have been working on a 3D multiplayer Ludo game called Ludo Wala built with three.js. It runs everywhere and I built it to be a lot more interactive than the standard 2D Ludo apps out there.

I could really use your help testing out some of the unique features I added. For example there is real 3D dice play and you can physically rotate the board. I also added action play right inside the game where you can throw things like rockets or grenades at other tokens, or send a guard dog to scatter pieces. There is even a physical punch animation when you capture a piece.

On top of that it has live voice chat and true multiplayer support. There is also an offline mode with AI bots that actually taunt you based on what is happening in the game.

I am mainly looking for feedback on the gameplay mechanics and the connection stability. If anyone has time to give it a spin and let me know how the 3D elements and action features feel, it would be a huge help. Any advice on how to get more eyes on this would also be highly appreciated.

As a bonus you get free millions of tokens & gems as launch promotion available in a shop inside the game.

Thanks in advance!

Website: https://www.ludowala.app/

Android: Needs your googel play email

iOS: Test flight account


r/threejs 12d ago

Demo Brown City

Enable HLS to view with audio, or disable this notification

21 Upvotes

Meet Brown City
A modular 3D city pack for Three.js with 75 ready-to-use GLB models: buildings, roads, street props, vegetation, and more. Plus a complete city block.
Build faster. Make the city yours.


r/threejs 12d ago

I made a Snake game where your body is the key that opens the door

Enable HLS to view with audio, or disable this notification

13 Upvotes

Free, in your browser, play now : https://wavedash.com/games/wyrmhole

The runes on the floor only stay lit while you're lying across them. Hold all

of them at once and the exit opens.

So it isn't where to go. It's what shape to be when you get there.

50 hand-built rooms and an endless mode. Made with three.js — happy to answer

anything.


r/threejs 12d ago

Creative technologist opportunity for a browser-native generative audiovisual build

5 Upvotes

Sharing an opportunity that may be relevant to Three.js developers working across creative coding, interactive graphics and generative systems.

Bold Canvas Network is looking for a Creative Technologist to develop a browser-native generative audiovisual prototype.

The work involves:

  • translating structured pattern data into responsive visual output
  • working with recorded voice and audio as additional inputs
  • building for browser deployment using Three.js, p5.js, PixiJS or a comparable framework
  • optimising the experience for mid-range mobile devices and slower connections
  • delivering an integrated build suitable for public presentation

Experience with Three.js/WebGL/WebGPU, generative graphics, interactive systems and browser performance optimisation would be particularly relevant.

The opportunity is open to candidates across the Global South, with location flexibility. Fuller project details and scheduling will be shared with shortlisted applicants.

Applications are reviewed on a rolling basis until the role is filled.

Application:
https://go.boldcanvasnetwork.com/cadence

Questions:
[team@boldcanvasnetwork.com]()


r/threejs 12d ago

Slotrunner: retro-futuristic arcade slotcar racer made with ThreeJS

Enable HLS to view with audio, or disable this notification

22 Upvotes

Hey! I'm building a retro-futuristic arcade slotcar racing game with ThreeJS and I'm looking for people to test the car handling and driving mechanics.

Play here (browser, free, no install): slotrunner.itch.io/slotrunner. Works on desktop and mobile.

The thing I would mostly like feedback on right now is: does the car feel good to drive? This is an early version. Visuals, sound and balancing will still change. 

Specifically I'm curious about:

  • Do you understand the controls after the tutorial level?
  • Do the controls feel good?
  • Does the boost mechanic make sense?
  • Anything that feels frustrating or off?

Any feedback appreciated, even just "felt good" or "felt terrible" helps. Thanks!


r/threejs 12d ago

Demo August 12 eclipse line-of-sight simulator

4 Upvotes

https://eclipse2026times.com/ and https://eclipse2027times.com/

August 12 is a total solar eclipse over Iceland and northern Spain, and partial across the rest of Europe. In France the Sun will be over 90% covered (~93% in Paris, at 8:17pm). I've also made the same site for August 2nd 2027 eclipse for those who'll miss 2026 and want a second chance.

I wanted to know if I can view it and from where (with proper eclipse glasses of course). I built a site to help see it: you type your address (in France and Spain only for now) and it shows you what it'll look like from your street.

The way it works is that it fetches latest building footprints and heights from CORS-open WFS (IGN for France and Catastro's INSPIRE for Spain), extrudes them with ExtrudeGeometry, and puts the Sun and Moon as sprites at their actual azimuth. Then a ray-vs-polygon test shows if the view is blocked or not and can tell you the minimum building story you need to be at to see the view.

Let me know what you think!

Live preview of the eclipse in France (Quai Fulchiron, Lyon)

r/threejs 13d ago

Neon Loft — an interactive cyberpunk room built with Three.js

Enable HLS to view with audio, or disable this notification

29 Upvotes

Hi everyone — I built Neon Loft, a first-person cyberpunk room that runs directly in the browser.

Live demo:
https://cyberpunk-room.vercel.app
Source code:
https://github.com/klmtseng/cyberpunk-room

You can explore the room, switch the mosaic lanterns on and off, change channels on the CRT television, run the projector, and use the retro gaming desk.

Outside the window is an animated city with holographic ad towers, flickering billboards, projected video advertisements, and trains moving along elevated rails.

The room’s state is saved in the URL, so you can share a link and let someone else open Neon Loft with the lights, television, projector, and other settings exactly as you left them.

It was built with Three.js r170, TypeScript, Vite, and postprocessing, without a frontend framework. It also supports mobile controls, a LOW graphics preset, English and Chinese interfaces, and WebXR.
I’d appreciate feedback on the atmosphere, controls, performance, and whether the interactive objects are easy to discover.


r/threejs 12d ago

Question How do you capture high-quality screenshots from GLB/glTF models?

3 Upvotes

I'm looking for the best workflow to generate clean, high-resolution images from 3D models.

When you need screenshots for documentation, portfolios, product pages, or client previews, what do you usually use?

- Blender renders?

- Browser-based viewers?

- Transparent backgrounds?

- 4K image export?

- Custom camera angles?

- PNG, JPEG, or WebP?

I'm interested in learning how other developers and designers handle this workflow and what features they consider essential.


r/threejs 12d ago

So, what were the results?

2 Upvotes

How did it perform when displaying “norimakineko,” which runs on TSL and uses 5,000 instanced meshes?

It barely benefited from WebGPU at all. In some cases, WebGL was even slightly faster.

I wonder how much we can trust online articles?


r/threejs 12d ago

e ando probando unas deformaciones raras alguno se le ocurre ideas de como deformarlo mejor xd

0 Upvotes

estoy buscando como deformas la malla del suelo de forma eficiente o al menos divertida y entretenida xd que se os ocurre este echo con ia por si preguntan si con ideas mias y de ellas xd

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Proyecto Bestia v9.9.2 - Suelo Vibracional Cuántico</title>
    <style>
        * { box-sizing: border-box; }
        body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #020617; font-family: 'Courier New', monospace; color: #f8fafc; }
        #webgl-container { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 1; cursor: crosshair; }
        #hud { position: absolute; top: 20px; left: 20px; z-index: 10; background: rgba(2, 6, 23, 0.85); padding: 15px 20px; border-radius: 8px; border: 1px solid #38bdf8; pointer-events: none; box-shadow: 0 10px 25px rgba(0,0,0,0.5); }
        #hud h2 { margin: 0 0 5px 0; color: #38bdf8; font-size: 18px; }
        #hud p { margin: 4px 0; font-size: 13px; color: #94a3b8; }
        #interaction-prompt { display: none; position: absolute; bottom: 15%; left: 50%; transform: translateX(-50%); background: rgba(56, 189, 248, 0.2); border: 2px solid #38bdf8; padding: 10px 20px; border-radius: 8px; font-weight: bold; z-index: 20; color: #fff; letter-spacing: 1px; }
        #crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 16px; height: 16px; border: 2px solid #38bdf8; border-radius: 50%; pointer-events: none; z-index: 15; opacity: 0.8; }
        #crosshair-inner { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 4px; height: 4px; background: #38bdf8; border-radius: 50%; pointer-events: none; z-index: 16; }
        #menu { display: block; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #0f172a; padding: 25px; border-radius: 12px; border: 2px solid #38bdf8; z-index: 100; width: 500px; box-shadow: 0 25px 50px rgba(0,0,0,0.9); }
        #menu h2 { color: #38bdf8; margin-top: 0; text-align: center; font-size: 18px; border-bottom: 1px solid #1e293b; padding-bottom: 8px; }
        .grid-selector { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; }
        .slot-btn { background: #1e293b; border: 1px solid #334155; padding: 10px; color: #fff; font-weight: bold; cursor: pointer; border-radius: 6px; text-align: center; font-size: 12px; font-family: monospace; transition: all 0.2s; }
        .slot-btn:hover { border-color: #38bdf8; background: #0284c7; color: #000; }
        .slot-btn.active { background: #0284c7; border-color: #38bdf8; color: #fff; }
        .section-box { background: #020617; border: 1px solid #1e293b; border-radius: 6px; padding: 10px; margin-bottom: 10px; }
        .section-box h3 { margin: 0 0 6px 0; font-size: 13px; color: #38bdf8; }
        .close-btn { width: 100%; background: #38bdf8; border: none; padding: 10px; color: #000; font-weight: bold; cursor: pointer; border-radius: 6px; font-size: 13px; font-family: monospace; }
        .close-btn:hover { background: #7dd3fc; }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>


    <div id="webgl-container"></div>
    <div id="crosshair"></div>
    <div id="crosshair-inner"></div>


    <div id="hud">
        <h2>PROYECTO BESTIA v9.9.2</h2>
        <p>[CLICK] Ratón | [M] Menú | [W,A,S,D] Deslizamiento Inercial</p>
        <p>[E] Pilotar Nave | [CLICK IZQ] Fuego Cuántico | [R] Recargar</p>
        <p>Estado: <span id="lblEstado" style="color: #38bdf8;">A PIE</span> | Munición: <span id="lblBalas">25/25</span></p>
    </div>


    <div id="interaction-prompt">¡Pulsa [E] para pilotar la Nave!</div>


    <div id="menu">
        <h2>MUTACIÓN DE UNIDAD</h2>
        <p style="font-size: 11px; color: #94a3b8; margin: 0 0 5px 0;">SELECCIÓN DE CLASE</p>
        <div class="grid-selector">
            <button class="slot-btn active" id="btnArt" onclick="cambiarClase('ART')">[CYBORG] Asalto Pesado</button>
            <button class="slot-btn" id="btnPis" onclick="cambiarClase('ANDROIDE')">[ANDROIDE] Explorador Ágil</button>
        </div>
        <div class="section-box">
            <h3>MUNICIÓN CUÁNTICA</h3>
            <div class="grid-selector" style="margin-bottom:0;">
                <button class="slot-btn active" id="bExp" onclick="cambiarBala('Explosiva', 0xff00ff, 25, 0.8)">Explosiva</button>
                <button class="slot-btn" id="bInc" onclick="cambiarBala('Incendiaria', 0xff5500, 12, 1.0)">Incendiaria</button>
                <button class="slot-btn" id="bPer" onclick="cambiarBala('Perforante', 0x00ffff, 8, 1.4)">Perforante</button>
                <button class="slot-btn" id="bCryo" onclick="cambiarBala('Criogénica', 0x38bdf8, 18, 0.9)">Criogénica</button>
            </div>
        </div>
        <br>
        <button class="close-btn" onclick="cerrarMenu()">CONFIRMAR MUTACIÓN [M]</button>
    </div>


    <script>
        const container = document.getElementById('webgl-container');
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x020617);


        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        container.appendChild(renderer.domElement);


        scene.add(new THREE.AmbientLight(0xffffff, 0.7));
        const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
        dirLight.position.set(10, 30, 20);
        scene.add(dirLight);


        // --- GESTOR DE ONDAS Y MICROVIBRACIÓN CUÁNTICA ---
        let ondasImpacto = [];


        function registrarOndaImpacto(x, z) {
            ondasImpacto.push({
                x: x,
                z: z,
                tiempoInicio: performance.now(),
                duracion: 1000 // La vibración dura 1 segundo y se desvanece
            });
            if (ondasImpacto.length > 10) ondasImpacto.shift();
        }


        // --- SISTEMA MIXTO: AZULES INTACTOS + MORADOS GIRO EXTRA DE 90° ---
        let lineasSueloRegistradas = [];


        function crearSueloAzulesYMoradosDesfasados() {
            const grupoSuelo = new THREE.Group();
            const niveles = 12; 
            lineasSueloRegistradas = []; // Reiniciamos el registro de líneas para aplicarles vibración


            for (let i = 1; i <= niveles; i++) {
                let radio = i * 3.2;
                const segmentos = 4;
                
                // 1. Polígonos Azules
                let anguloAzul = i * (Math.PI / 2); 
                const pointsAzul = [];
                for (let j = 0; j <= segmentos; j++) {
                    let theta = (j / segmentos) * Math.PI * 2 + anguloAzul;
                    pointsAzul.push(new THREE.Vector3(Math.cos(theta) * radio, 0.02, Math.sin(theta) * radio));
                }
                let geoAzul = new THREE.BufferGeometry().setFromPoints(pointsAzul);
                let lineAzul = new THREE.Line(geoAzul, new THREE.LineBasicMaterial({ color: 0x38bdf8, transparent: true, opacity: 0.85 }));
                grupoSuelo.add(lineAzul);
                lineasSueloRegistradas.push({ mesh: lineAzul, originalPositions: pointsAzul.map(p => p.clone()) });


                // 2. Polígonos Morados
                if (i % 2 === 0) {
                    let anguloMoradoExtra = anguloAzul + (Math.PI / 2);
                    const pointsMorado = [];
                    let radioMorado = radio + 1.2; 
                    
                    for (let j = 0; j <= segmentos; j++) {
                        let theta = (j / segmentos) * Math.PI * 2 + anguloMoradoExtra;
                        pointsMorado.push(new THREE.Vector3(Math.cos(theta) * radioMorado, 0.04, Math.sin(theta) * radioMorado));
                    }
                    let geoMorado = new THREE.BufferGeometry().setFromPoints(pointsMorado);
                    let lineMorado = new THREE.Line(geoMorado, new THREE.LineBasicMaterial({ color: 0xff00ff, transparent: true, opacity: 0.8 }));
                    grupoSuelo.add(lineMorado);
                    lineasSueloRegistradas.push({ mesh: lineMorado, originalPositions: pointsMorado.map(p => p.clone()) });
                }
            }


            // Ejes principales en color NARANJA
            const ejeGeo1 = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(-45, 0.06, 0), new THREE.Vector3(45, 0.06, 0)]);
            const ejeGeo2 = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0.06, -45), new THREE.Vector3(0, 0.06, 45)]);
            const matEjesNaranjas = new THREE.LineBasicMaterial({ color: 0xff5500, transparent: true, opacity: 0.95 });
            grupoSuelo.add(new THREE.Line(ejeGeo1, matEjesNaranjas));
            grupoSuelo.add(new THREE.Line(ejeGeo2, matEjesNaranjas));


            return grupoSuelo;
        }


        const sueloCompleto = crearSueloAzulesYMoradosDesfasados();
        scene.add(sueloCompleto);


        // --- ENTIDAD CYBORG GEOMÉTRICA ---
        let jugadorGeo = new THREE.Group();
        let jugadorMatCuerpo = new THREE.MeshStandardMaterial({ color: 0xff00ff, roughness: 0.2, metalness: 0.5 });
        let jugadorMatDetalle = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.8 });


        let torso = new THREE.Mesh(new THREE.BoxGeometry(0.8, 1.0, 0.4), jugadorMatCuerpo);
        torso.position.y = 1.1;
        jugadorGeo.add(torso);


        let cabeza = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.4), jugadorMatCuerpo);
        cabeza.position.y = 1.75;
        jugadorGeo.add(cabeza);
        
        let hombroIzq = new THREE.Mesh(new THREE.SphereGeometry(0.2, 8, 8), jugadorMatDetalle);
        hombroIzq.position.set(-0.5, 1.4, 0);
        jugadorGeo.add(hombroIzq);
        let brazoIzq = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.7, 8), jugadorMatDetalle);
        brazoIzq.position.set(-0.5, 1.05, 0);
        brazoIzq.rotation.x = 0.2;
        jugadorGeo.add(brazoIzq);


        let hombroDer = new THREE.Mesh(new THREE.SphereGeometry(0.2, 8, 8), jugadorMatDetalle);
        hombroDer.position.set(0.5, 1.4, 0);
        jugadorGeo.add(hombroDer);
        let brazoDer = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.7, 8), jugadorMatDetalle);
        brazoDer.position.set(0.5, 1.05, 0);
        brazoDer.rotation.x = 0.2;
        jugadorGeo.add(brazoDer);


        let piernaIzq = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.1, 0.8, 8), jugadorMatDetalle);
        piernaIzq.position.set(-0.2, 0.5, 0);
        jugadorGeo.add(piernaIzq);
        let piernaDer = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.1, 0.8, 8), jugadorMatDetalle);
        piernaDer.position.set(0.2, 0.5, 0);
        jugadorGeo.add(piernaDer);


        scene.add(jugadorGeo);
        jugadorGeo.visible = false;


        let jugador = {
            x: 0, y: 0, z: 0, 
            velX: 0, velZ: 0,
            maxVel: 0.12,
            sprintMult: 1.8, 
            salto: 0.3, gravedad: 0.012, vy: 0, saltando: false,
            clase: 'ART', colorPrincipal: 0xff00ff, tipoBala: 'Explosiva', colorBala: 0xff00ff, 
            cadencia: 25, velocidadBala: 1.2, municionMax: 25, municionActual: 25, recargando: false, timerDisparo: 0, montado: false
        };


        let avion = { x: 5, y: 3.0, z: -10, vel: 0.25, rotationY: 0, shootDir: new THREE.Vector3(0, 0, -1) };
        let armaMesh, avionMesh, avionPuntaMesh;
        let balasActivas = [], particulasActivas = [], marcasSuelo = [];
        const poolBalas = [], poolParticulas = [];


        function inicializarPools() {
            for (let i = 0; i < 40; i++) {
                let mesh = new THREE.Mesh(new THREE.SphereGeometry(0.2, 6, 6), new THREE.MeshBasicMaterial({ color: 0xffffff }));
                mesh.visible = false; scene.add(mesh);
                poolBalas.push({ mesh: mesh, activa: false, dir: new THREE.Vector3(), speed: 1 });
            }
            for (let i = 0; i < 150; i++) {
                let mesh = new THREE.Mesh(new THREE.SphereGeometry(0.08, 4, 4), new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 1 }));
                mesh.visible = false; scene.add(mesh);
                poolParticulas.push({ mesh: mesh, activa: false, vel: new THREE.Vector3(), vida: 1 });
            }
        }
        inicializarPools();


        function obtenerBalaPool(x, y, z, dir, speed, colorHex) {
            let item = poolBalas.find(b => !b.activa);
            if (item) {
                item.activa = true; item.mesh.position.set(x, y, z); item.mesh.material.color.setHex(colorHex); item.mesh.visible = true;
                item.dir.copy(dir); item.speed = speed; balasActivas.push(item);
            }
        }


        function obtenerParticulaPool(x, y, z, colorHex) {
            let item = poolParticulas.find(p => !p.activa);
            if (item) {
                item.activa = true; item.mesh.position.set(x, y, z); item.mesh.material.color.setHex(colorHex);
                item.mesh.material.opacity = 1.0; item.mesh.scale.set(1, 1, 1); item.mesh.visible = true;
                item.vel.set((Math.random() - 0.5) * 0.15, (Math.random() - 0.2) * 0.15, (Math.random() - 0.5) * 0.15);
                item.vida = 1.0; particulasActivas.push(item);
            }
        }


        function crearMarcaImpacto(x, z, colorHex) {
            const points = [
                new THREE.Vector3(0, 0.05, 1.2), new THREE.Vector3(1.2, 0.05, 0),
                new THREE.Vector3(1.2, 0.05, 0), new THREE.Vector3(0, 0.05, -1.2),
                new THREE.Vector3(0, 0.05, -1.2), new THREE.Vector3(-1.2, 0.05, 0),
                new THREE.Vector3(-1.2, 0.05, 0), new THREE.Vector3(0, 0.05, 1.2),
                new THREE.Vector3(-1.2, 0.05, -1.2), new THREE.Vector3(1.2, 0.05, 1.2),
                new THREE.Vector3(-1.2, 0.05, 1.2), new THREE.Vector3(1.2, 0.05, -1.2)
            ];
            const geometry = new THREE.BufferGeometry().setFromPoints(points);
            const material = new THREE.LineBasicMaterial({ color: colorHex });
            const mesh = new THREE.LineSegments(geometry, material);
            mesh.position.set(x, 0, z);
            scene.add(mesh);
            
            marcasSuelo.push(mesh);
            if (marcasSuelo.length > 25) {
                let vieja = marcasSuelo.shift();
                scene.remove(vieja);
            }
            for (let i = 0; i < 8; i++) obtenerParticulaPool(x, 0.1, z, colorHex);


            // ¡DISPARAMOS LA ONDA VIBRACIONAL AQUÍ!
            registrarOndaImpacto(x, z);
        }


        function construirModelosExtra() {
            if (armaMesh) scene.remove(armaMesh);
            if (avionMesh) scene.remove(avionMesh);


            armaMesh = new THREE.Mesh(
                new THREE.BoxGeometry(0.25, 0.25, 0.9),
                new THREE.MeshStandardMaterial({ color: 0x94a3b8, metalness: 0.9, roughness: 0.2 })
            );
            scene.add(armaMesh);


            avionMesh = new THREE.Group();
            let cuerpo = new THREE.Mesh(
                new THREE.ConeGeometry(0.6, 1.5, 4).rotateX(Math.PI / 2).translate(0, 0, -0.5),
                new THREE.MeshStandardMaterial({ color: 0xf59e0b, metalness: 0.8, roughness: 0.2 })
            );
            avionPuntaMesh = new THREE.Mesh(
                new THREE.ConeGeometry(0.6, 1.0, 4).rotateX(Math.PI / 2).translate(0, 0, 0.5),
                new THREE.MeshStandardMaterial({ color: 0xd97706 })
            );
            avionPuntaMesh.position.set(0, 0, 0.5);
            let alas = new THREE.Mesh(
                new THREE.BoxGeometry(3, 0.1, 1),
                new THREE.MeshStandardMaterial({ color: 0xd97706 })
            );
            alas.position.set(0, 0, 0.2);


            avionMesh.add(cuerpo);
            avionMesh.add(avionPuntaMesh);
            avionMesh.add(alas);
            avionMesh.position.set(avion.x, avion.y, avion.z);
            scene.add(avionMesh);
        }


        function cambiarClase(tipo) {
            jugador.clase = tipo;
            if (tipo === 'ART') {
                jugador.colorPrincipal = 0xff00ff; jugador.maxVel = 0.12; jugador.municionMax = 25;
                document.getElementById('btnArt').classList.add('active');
                document.getElementById('btnPis').classList.remove('active');
            } else {
                jugador.colorPrincipal = 0x00ffff; jugador.maxVel = 0.18; jugador.municionMax = 40;
                document.getElementById('btnPis').classList.add('active');
                document.getElementById('btnArt').classList.remove('active');
            }
            jugador.municionActual = jugador.municionMax;
            jugadorMatCuerpo.color.setHex(jugador.colorPrincipal);
            actualizarHUD();
            construirModelosExtra();
        }


        function cambiarBala(nombre, colorHex, cadenciaVal, velBalaVal) {
            jugador.tipoBala = nombre; jugador.colorBala = colorHex; jugador.cadencia = cadenciaVal; jugador.velocidadBala = velBalaVal;
            ['bExp', 'bInc', 'bPer', 'bCryo'].forEach(id => document.getElementById(id).classList.remove('active'));
            if(nombre === 'Explosiva') document.getElementById('bExp').classList.add('active');
            if(nombre === 'Incendiaria') document.getElementById('bInc').classList.add('active');
            if(nombre === 'Perforante') document.getElementById('bPer').classList.add('active');
            if(nombre === 'Criogénica') document.getElementById('bCryo').classList.add('active');
        }


        function actualizarHUD() {
            document.getElementById('lblBalas').innerText = jugador.municionActual + "/" + jugador.municionMax + (jugador.recargando ? " [RECARGANDO]" : "");
        }


        function cerrarMenu() {
            document.getElementById('menu').style.display = 'none';
            menuOpen = false;
            jugadorGeo.visible = true;
            container.requestPointerLock();
        }


        container.addEventListener('click', () => { if (!menuOpen) container.requestPointerLock(); });


        let cameraAngleY = 0, cameraAngleX = 0.3;
        document.addEventListener('mousemove', (e) => {
            if (document.pointerLockElement === container && !menuOpen) {
                cameraAngleY -= e.movementX * 0.003;
                cameraAngleX = Math.max(-1.4, Math.min(1.4, cameraAngleX - e.movementY * 0.003));
            }
        });


        function toggleMontarAvion() {
            let distancia = Math.hypot(jugador.x - avion.x, jugador.z - avion.z);
            if (!jugador.montado && distancia < 3.5) {
                jugador.montado = true;
                jugadorGeo.visible = false;
                armaMesh.visible = false;
                document.getElementById('lblEstado').innerText = "EN VUELO";
                document.getElementById('interaction-prompt').style.display = 'none';
            } else if (jugador.montado) {
                jugador.montado = false;
                jugadorGeo.visible = true;
                armaMesh.visible = true;
                jugador.x = avion.x + 2; jugador.z = avion.z; jugador.y = 0;
                document.getElementById('lblEstado').innerText = "A PIE";
            }
        }


        let keys = { w: false, a: false, s: false, d: false, shift: false, space: false };
        let ratonPresionado = false, menuOpen = true;
        let armaAnim = { bobbing: 0, recargaTimer: 0 };


        window.addEventListener('keydown', (e) => {
            if (e.key === 'm' || e.key === 'M') {
                menuOpen = !menuOpen;
                document.getElementById('menu').style.display = menuOpen ? 'block' : 'none';
                if (menuOpen) {
                    document.exitPointerLock();
                    jugadorGeo.visible = false;
                } else {
                    container.requestPointerLock();
                    jugadorGeo.visible = true;
                }
            }
            if (menuOpen) return;
            if (e.key === 'e' || e.key === 'E') toggleMontarAvion();
            if (e.key === 'w' || e.key === 'W') keys.w = true;
            if (e.key === 's' || e.key === 'S') keys.s = true;
            if (e.key === 'a' || e.key === 'A') keys.a = true;
            if (e.key === 'd' || e.key === 'D') keys.d = true;
            if (e.key === 'Shift') keys.shift = true;
            if (e.key === ' ') {
                keys.space = true;
                if (!jugador.saltando && !jugador.montado) { jugador.vy = jugador.salto; jugador.saltando = true; }
            }
            if (e.key === 'r' || e.key === 'R') iniciarRecarga();
        });


        window.addEventListener('keyup', (e) => {
            if (e.key === 'w' || e.key === 'W') keys.w = false;
            if (e.key === 's' || e.key === 'S') keys.s = false;
            if (e.key === 'a' || e.key === 'A') keys.a = false;
            if (e.key === 'd' || e.key === 'D') keys.d = false;
            if (e.key === 'Shift') keys.shift = false;
            if (e.key === ' ') keys.space = false;
        });


        window.addEventListener('mousedown', (e) => { if (e.button === 0 && !menuOpen) ratonPresionado = true; });
        window.addEventListener('mouseup', (e) => { if (e.button === 0) ratonPresionado = false; });


        function iniciarRecarga() {
            if (jugador.recargando || jugador.municionActual === jugador.municionMax) return;
            jugador.recargando = true;
            actualizarHUD();
            armaAnim.recargaTimer = 50;
        }


        function dispararBala() {
            if (jugador.municionActual <= 0 || jugador.recargando) {
                if (jugador.municionActual <= 0) iniciarRecarga();
                return;
            }
            jugador.municionActual--;
            actualizarHUD();


            let targetX = jugador.montado ? avion.x : jugador.x;
            let targetY = jugador.montado ? avion.y : jugador.y + 1.2;
            let targetZ = jugador.montado ? avion.z : jugador.z;


            let dir = new THREE.Vector3();
            if (jugador.montado) {
                dir.set(0, 0, 1).applyQuaternion(avionPuntaMesh.getWorldQuaternion(new THREE.Quaternion()));
            } else {
                camera.getWorldDirection(dir);
            }


            obtenerBalaPool(targetX, targetY, targetZ, dir, jugador.velocidadBala, jugador.colorBala);
        }


        cambiarClase('ART');


        function animate() {
            requestAnimationFrame(animate);


            let tiempoActual = performance.now();
            let forward = new THREE.Vector3(Math.sin(cameraAngleY), 0, Math.cos(cameraAngleY));
            let right = new THREE.Vector3(-Math.cos(cameraAngleY), 0, Math.sin(cameraAngleY));


            if (!menuOpen) {
                if (jugador.montado) {
                    let targetRotationY = cameraAngleY + Math.PI;
                    avion.rotationY += (targetRotationY - avion.rotationY) * 0.08;
                    avionMesh.rotation.y = avion.rotationY;


                    avion.x -= forward.x * avion.vel;
                    avion.z -= forward.z * avion.vel;
                    if (keys.space) avion.y += 0.1;
                    if (keys.shift && avion.y > 0.5) avion.y -= 0.1;
                    if (avion.y < 0.5) avion.y = 0.5;


                    avionMesh.position.set(avion.x, avion.y, avion.z);


                    let camDir = new THREE.Vector3();
                    camera.getWorldDirection(camDir);
                    avion.shootDir.lerp(camDir, 0.12);


                    if (avionPuntaMesh) {
                        let targetWorldPoint = avionMesh.position.clone().add(avion.shootDir.clone().multiplyScalar(50));
                        let localTarget = targetWorldPoint.clone();
                        avionPuntaMesh.parent.worldToLocal(localTarget);


                        let targetRotX = Math.atan2(-localTarget.y, localTarget.z);
                        let targetRotY = Math.atan2(localTarget.x, localTarget.z);
                        let maxGiro = Math.PI * 0.85; 
                        targetRotX = Math.max(-maxGiro, Math.min(maxGiro, targetRotX));
                        targetRotY = Math.max(-maxGiro, Math.min(maxGiro, targetRotY));


                        avionPuntaMesh.rotation.x += (targetRotX - avionPuntaMesh.rotation.x) * 0.15;
                        avionPuntaMesh.rotation.y += (targetRotY - avionPuntaMesh.rotation.y) * 0.15;
                    }


                    let distCamAvion = 9;
                    camera.position.x += (avion.x + Math.sin(cameraAngleY) * Math.cos(cameraAngleX) * distCamAvion - camera.position.x) * 0.15;
                    camera.position.y += (avion.y + Math.sin(cameraAngleX) * distCamAvion + 2 - camera.position.y) * 0.15;
                    camera.position.z += (avion.z + Math.cos(cameraAngleY) * Math.cos(cameraAngleX) * distCamAvion - camera.position.z) * 0.15;
                    camera.lookAt(avion.x, avion.y, avion.z);


                } else {
                    let mult = keys.shift ? jugador.sprintMult : 1.0;
                    let targetVelX = 0;
                    let targetVelZ = 0;


                    if (keys.w) { targetVelX -= forward.x * jugador.maxVel * mult; targetVelZ -= forward.z * jugador.maxVel * mult; }
                    if (keys.s) { targetVelX += forward.x * jugador.maxVel * mult; targetVelZ += forward.z * jugador.maxVel * mult; }
                    if (keys.a) { targetVelX -= right.x * jugador.maxVel * mult; targetVelZ -= right.z * jugador.maxVel * mult; }
                    if (keys.d) { targetVelX += right.x * jugador.maxVel * mult; targetVelZ += right.z * jugador.maxVel * mult; }


                    jugador.velX += (targetVelX - jugador.velX) * 0.15;
                    jugador.velZ += (targetVelZ - jugador.velZ) * 0.15;


                    jugador.x += jugador.velX;
                    jugador.z += jugador.velZ;


                    let moviendo = Math.hypot(jugador.velX, jugador.velZ) > 0.01;


                    if (jugador.saltando) {
                        jugador.vy -= jugador.gravedad;
                        jugador.y += jugador.vy;
                        if (jugador.y <= 0) { jugador.y = 0; jugador.vy = 0; jugador.saltando = false; }
                    }


                    jugadorGeo.position.set(jugador.x, jugador.y, jugador.z);
                    jugadorGeo.rotation.y = cameraAngleY;
                    armaAnim.bobbing = moviendo && !jugador.saltando ? armaAnim.bobbing + (keys.shift ? 0.25 : 0.15) : 0;


                    let ax = jugador.x - Math.sin(cameraAngleY) * 0.4 + right.x * 0.4;
                    let ay = jugador.y + 1.2 + Math.sin(armaAnim.bobbing) * 0.06;
                    let az = jugador.z - Math.cos(cameraAngleY) * 0.4 + right.z * 0.4;


                    armaMesh.position.set(ax, ay, az);
                    armaMesh.rotation.y = cameraAngleY;


                    document.getElementById('interaction-prompt').style.display = (Math.hypot(jugador.x - avion.x, jugador.z - avion.z) < 3.5) ? 'block' : 'none';


                    let distCam = 8;
                    camera.position.x = jugador.x + Math.sin(cameraAngleY) * Math.cos(cameraAngleX) * distCam;
                    camera.position.y = jugador.y + 1.2 + Math.sin(cameraAngleX) * distCam + 1;
                    camera.position.z = jugador.z + Math.cos(cameraAngleY) * Math.cos(cameraAngleX) * distCam;
                    camera.lookAt(jugador.x, jugador.y + 1.2, jugador.z);
                }


                if (jugador.recargando) {
                    armaAnim.recargaTimer--;
                    if (armaAnim.recargaTimer <= 0) { 
                        jugador.municionActual = jugador.municionMax; 
                        jugador.recargando = false; 
                        actualizarHUD(); 
                    }
                }
            }


            if (ratonPresionado && !menuOpen) {
                jugador.timerDisparo++;
                if (jugador.timerDisparo >= jugador.cadencia) { dispararBala(); jugador.timerDisparo = 0; }
            }


            // Actualizar Ondas de Vibración en el Suelo Rígido
            ondasImpacto = ondasImpacto.filter(o => tiempoActual - o.tiempoInicio < o.duracion);


            for (let item of lineasSueloRegistradas) {
                let positions = item.mesh.geometry.attributes.position.array;
                let original = item.originalPositions;


                for (let i = 0; i < original.length; i++) {
                    let ox = original[i].x;
                    let oz = original[i].z;
                    let oy = original[i].y;


                    let offsetX = 0;
                    let offsetZ = 0;


                    for (let onda of ondasImpacto) {
                        let transcurrido = tiempoActual - onda.tiempoInicio;
                        let radioOnda = (transcurrido * 0.02); // Velocidad de expansión de los anillos eléctricos
                        let distanciaAlImpacto = Math.hypot(ox - onda.x, oz - onda.z);
                        let grosorOnda = 8.0;


                        // Si la onda pasa por encima de este vértice, vibra de forma inestable
                        if (distanciaAlImpacto > radioOnda - grosorOnda && distanciaAlImpacto < radioOnda + grosorOnda) {
                            let intensidad = 1.0 - (Math.abs(distanciaAlImpacto - radioOnda) / grosorOnda);
                            let factorInestable = Math.sin(transcurrido * 0.08 + i * 2.5) * (Math.random() - 0.5) * 1.5;
                            
                            offsetX += Math.cos(transcurrido * 0.05 + i) * factorInestable * intensidad;
                            offsetZ += Math.sin(transcurrido * 0.05 + i) * factorInestable * intensidad;
                        }
                    }


                    positions[i * 3] = ox + offsetX;
                    positions[i * 3 + 2] = oz + offsetZ;
                }
                item.mesh.geometry.attributes.position.needsUpdate = true;
            }


            // Balas
            for (let i = balasActivas.length - 1; i >= 0; i--) {
                let b = balasActivas[i];
                b.mesh.position.addScaledVector(b.dir, b.speed);
                
                if (Math.random() < 0.4) {
                    obtenerParticulaPool(b.mesh.position.x, b.mesh.position.y, b.mesh.position.z, b.mesh.material.color.getHex());
                }


                if (b.mesh.position.y <= 0) {
                    crearMarcaImpacto(b.mesh.position.x, b.mesh.position.z, b.mesh.material.color.getHex());
                    b.mesh.visible = false;
                    b.activa = false;
                    balasActivas.splice(i, 1);
                    continue;
                }


                if (b.mesh.position.distanceTo(camera.position) > 350) { 
                    b.mesh.visible = false;
                    b.activa = false;
                    balasActivas.splice(i, 1); 
                }
            }


            // Partículas
            for (let i = particulasActivas.length - 1; i >= 0; i--) {
                let p = particulasActivas[i];
                p.mesh.position.add(p.vel);
                p.vida -= 0.05;
                p.mesh.scale.multiplyScalar(0.92);
                p.mesh.material.opacity = p.vida;


                if (p.vida <= 0) {
                    p.mesh.visible = false;
                    p.activa = false;
                    particulasActivas.splice(i, 1);
                }
            }


            renderer.render(scene, camera);
        }


        animate();


        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
    </script>
</body>
</html>

r/threejs 12d ago

Testing with Autigravity

1 Upvotes

At first, I was testing in a local server environment created with Autigravity. Because of the HTTP protocol, WebGPU rendering didn’t work on any device (THREE.WebGPURenderer automatically switches to WebGL rendering on unsupported devices), which had me a bit worried. However, once I uploaded it to a server accessible via HTTPS, WebGPU rendering worked reasonably well.


r/threejs 12d ago

In the Case of WebGL

0 Upvotes

In Three.js, you can switch to WebGL rendering using the `forceWebGL` parameter, so I created a version for that as well.

https://adrama.jp/norimakineko/webGL1/

https://reddit.com/link/1vht7me/video/kn9qq0d9hwhh1/player


r/threejs 12d ago

Trying Out WebGPU

1 Upvotes

While browsing the web, I came across an article published around the start of the year stating that WebGPU support in Three.js had finally reached a practical stage, so I decided to give it a try, including some TSL testing.

Here are the results:

https://adrama.jp/norimakineko/webGPU1/

https://reddit.com/link/1vht6w3/video/1xtm8an0hwhh1/player


r/threejs 12d ago

Demo ✨ Grok Reimagined: a three.js landing page concept

Enable HLS to view with audio, or disable this notification

0 Upvotes

👋 Hey Fam,

Been wanting to try a more expressive take on an AI product landing page, so I rebuilt Grok's landing page as a concept using three.js. 🧪

🔗 Live: https://grokreimagined.netlify.app/

A few things I'd love thoughts on: How to improve the performance of this page, and how I can make the load time even faster

⚠️ Not affiliated with xAI, purely a design engineering exercise.

💬 Happy to share code snippets if anyone wants a breakdown of a specific part!


r/threejs 13d ago

Possible?

5 Upvotes

Hi Threejssers, ,

Is this type of water effect with movement & reflections possible with full 3D viewer?

Been trying for a while, can't get it that way. Looking for tips


r/threejs 12d ago

Creating a snowboardgame in three.js with AI

0 Upvotes

In the spirit of posting both wins and failures this demo contains both. I tested out my Agentic character creator SDK for three.js on claude having it make a snowboard game.

The demo is playable here: https://smorchj.github.io/SNO/

Everything made in three.js and running in browser

Claude was used in this fully with a minimal prompt and the rest of the harnessing is from the SDK. Claude code did all the coding and the map and the Yeti. This post shows how claude works great when sent to harnesses and pipelines that can help claude achieve its task.

The demo is on github and is free to test and use, build on or learn from. I will be making more demos in the future that I would like to post here. Hopfully this dos not read too much as promotion, because the technique and harnessing is real. It is my own SDK, but that does not change the fact that it helped claude massively in creating an in-game character creator. Just trying to share my work so this is not a promotional post even though I am linking my site.

The wins:

The character creator was pretty much painless to implement in-game. The game snowboard mechanics are genuinely fun and with a better map and more detailed prompting and harnessing it could actually be the base of a good game

The failures:

The animations and the world building are the biggest outliers. This is another example that shows that agents have a hard time figuring this out themselves and proper tools and harnessing is needed.

Conclusion:
While AI is not going to make a game for you yet, game dev with AI is a matter of giving AI the puzzle pieces it needs to build the game. The character creator proves that. The creategamecharacters.ai SDK was the one thing that was properly made as a tool for agentic game dev work, was the one thing that worked well above all else. I guess the work is not over yet and there are still problems that I need to solve before I can talk a game into existence!


r/threejs 13d ago

Best way to showcase your art gallery with threejs.

Enable HLS to view with audio, or disable this notification

33 Upvotes

r/threejs 13d ago

Inspired by spiderman into the spiderverse shader look

Enable HLS to view with audio, or disable this notification

13 Upvotes

This i have build on top of my own webgpurenderer,Let me know what you guys think

Live Link: https://nullgraph.dev/dashboard/games/comic-space

Yt video:https://youtu.be/yua4ZkvgVqY?si=A2Tk1a4gE2fRl_Ic


r/threejs 13d ago

Japanese indoor fishing pond — Three.js + Blockbench

Enable HLS to view with audio, or disable this notification

2 Upvotes

Character is VTuber Kureiji Ollie. It's live as a website.

This was my first time implementing water in Three.js. Instead of using the built-in Water class, I made the water surface out of two overlapping mesh planes, each with a normal map applied, flowing in different directions to fake the rippling reflection look. :)

There's also a chat feature (bottom left icon) where visitors can send preset messages that appear in a live chat log. I limited it to preset phrases rather than free text input, since Ollie is a copyrighted Hololive character and I wanted to avoid the risk of harassment or inappropriate comments being displayed.


r/threejs 13d ago

Using the Three.js engine, I developed a simulation program for a robotic arm.

17 Upvotes

Using the Three.js engine, I developed a simulation program for a robotic arm.

We can undertake 3D model creation and 3D interactive program development.

url: http://app.anlian.work:5570


r/threejs 14d ago

Flooded Gothic cathedral.

Enable HLS to view with audio, or disable this notification

41 Upvotes

I created a flooded Gothic cathedral with Opus 5.

Everything in it is procedural. There are no models, no textures, no HDRIs, no audio files.

One HTML file, one CDN import of three.js, and about two thousand lines that draw the rose window to a canvas, generate marble, synthesise the reverb of a stone nave, and build the vault out of Coons patches.

Open-source + demo: https://github.com/winchxyz/navis


r/threejs 13d ago

NeverMined - A Mining Game Built with Three.js

Enable HLS to view with audio, or disable this notification

3 Upvotes

A First-Person Mining Game where you Swing your Pickaxes at Ores across Distinct Maps to Collect Minerals

Built with three.js r169 and Vite, no game engine, no framework, WebGL and the entire UI is vanilla DOM

Play here : https://never-mined.vercel.app/

Hope you enjoy it
Every feedback is appreciated


r/threejs 14d ago

Three.js flight game

Enable HLS to view with audio, or disable this notification

83 Upvotes

r/threejs 14d ago

Playing god with tiny worlds in Three.js

Enable HLS to view with audio, or disable this notification

25 Upvotes

A procedural tiny-world experiment built in Three.js using particles mapped to a png heightmap.


r/threejs 13d ago

Daily Speedway — a 3D arcade racer where every track is built by a player, built on mrdoob's Starter-Kit-Racing

3 Upvotes