| name | 3d-pixel-web |
| description | Create stunning 3D websites with pixel art and mosaic UI/UX design using Three.js. Use this skill when the user asks to build a 3D website or landing page, add Three.js 3D effects or scenes, create pixel art / mosaic / voxel-style web design, combine 3D rendering with retro or pixel aesthetics, animate 3D elements on a webpage, create immersive scrollytelling / Z-axis scrolljacking experiences, or build 2.5D HD-2D worlds mixing Low-Poly 3D environments with pixel art sprite billboards. Triggers include any mention of 3D site, Three.js, pixel design, mosaique 3D, effet 3D pixel, voxel web, CRT effect, scanline effect, scrollytelling, scrolljacking, défilement axe Z, 2.5D, HD-2D, sprite billboard, Low-Poly world, or caméra sur rail.
|
3D Pixel Web
Create immersive 3D websites combining Three.js rendering with pixel art / mosaic aesthetics.
Core Design Philosophy
- Pixel/Mosaic grid overlay on 3D scenes: use low-res canvas scaling, fragment shader posterization, or CSS pixelated filter
- Voxel/block geometry as primary 3D elements (BoxGeometry, InstancedMesh)
- Limited color palettes (8-32 colors) defined as JS constants
- Chunky typography: pixel fonts (Press Start 2P, VT323 from Google Fonts) over 3D canvas
- CRT / scanline post-process overlay for authenticity
antialias: false on WebGLRenderer to preserve hard pixel edges
Project Structure
index.html <- canvas + overlay HTML
style.css <- pixel font, body bg, canvas sizing, scanline overlay
main.js <- Three.js scene, animation loop, resize handler
shaders.js <- optional GLSL pixel/mosaic shaders
scrolly.js <- scrollytelling / scroll-driven camera rail (optional)
sprites.js <- 2.5D billboard sprites pixel art (optional)
Quick Start Boilerplate
<canvas id="canvas"></canvas>
<div id="ui-overlay"></div>
<div id="scanlines"></div>
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const canvas = document.getElementById('canvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const clock = new THREE.Clock();
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
renderer.setSize(innerWidth, innerHeight);
renderer.setAnimationLoop(() => {
const delta = clock.getDelta();
controls.update();
renderer.render(scene, camera);
});
Pixel / Mosaic Rendering Techniques
Low-Res Scale (simplest, most authentic)
canvas { image-rendering: pixelated; image-rendering: crisp-edges; }
renderer.setSize(innerWidth / 4, innerHeight / 4);
canvas.style.width = '100%';
canvas.style.height = '100%';
CSS Scanline Overlay
#scanlines {
position: fixed; inset: 0; pointer-events: none; z-index: 10;
background: repeating-linear-gradient(
0deg, transparent, transparent 2px,
rgba(0,0,0,0.15) 2px, rgba(0,0,0,0.15) 4px
);
}
GLSL Mosaic Postprocess Shader
See references/pixel-shaders.md for the complete mosaic fragment shader using THREE.EffectComposer.
Voxel / Block Scene Pattern
const geo = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshStandardMaterial();
const count = 500;
const mesh = new THREE.InstancedMesh(geo, mat, count);
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set(
Math.floor(Math.random() * 20) - 10,
Math.floor(Math.random() * 10),
Math.floor(Math.random() * 20) - 10
);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
mesh.setColorAt(i, new THREE.Color(PALETTE[i % PALETTE.length]));
}
mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);
Color Palettes
Always define palettes as constants, never use Math.random() for colors:
const PALETTE_NEON = ['#ff00ff', '#00ffff', '#ffff00', '#ff007f', '#7f00ff'];
const PALETTE_GAMEBOY = ['#0f380f', '#306230', '#8bac0f', '#9bbc0f'];
const PALETTE_CYBERPUNK = ['#0d0221', '#190535', '#6b1fb1', '#f72585', '#4cc9f0'];
const PALETTE_VAPORWAVE = ['#ff71ce', '#01cdfe', '#05ffa1', '#b967ff', '#fffb96'];
Lighting for Pixel Look
const ambient = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambient);
const dir = new THREE.DirectionalLight(0xffffff, 1.2);
dir.position.set(5, 10, 5);
scene.add(dir);
Performance Rules
- Use
InstancedMesh for any repeated geometry
- Call
geometry.dispose(); material.dispose(); texture.dispose() on cleanup
- Always use
renderer.setAnimationLoop not manual RAF
- Always use
clock.getDelta() for frame-independent animation
- Max
pixelRatio: 2
- Debug with
renderer.info (draw calls, triangles, textures in GPU memory)
Three.js Production Patterns
See references/threejs-patterns.md for complete production best practices:
- Resource cleanup & GPU memory leaks prevention
- Asset loading (GLTFLoader, DRACOLoader, TextureLoader, LoadingManager)
- Camera setup, near/far planes, z-fighting
- AnimationMixer usage
- OrbitControls damping
- HDR environment maps
Scrollytelling 3D — Z-Axis Scrolljacking
Le scroll de l'utilisateur est "capturé" (scrolljacking) pour déplacer une caméra virtuelle
sur un rail en profondeur (axe Z). L'utilisateur avance dans une scène 3D comme s'il
yoyageait dans un tunnel ou un couloir narratif.
Principes clés
- Désactiver le scroll natif du navigateur (
overflow: hidden sur body)
- Capturer
wheel et touchmove pour alimenter une variable scrollProgress (0 → 1)
- Utiliser
gsap ou une interpolation lerp manuelle pour une caméra fluide (inertie)
- Définir des waypoints (points clé) où des événements narratifs se déclenchent
- Ne JAMAIS utiliser
scrollY directement → toujours passer par une target + lerp
Boilerplate Scrollytelling
<canvas id="canvas"></canvas>
<div id="hud"></div>
body { margin: 0; overflow: hidden; background: #000; }
#canvas { display: block; image-rendering: pixelated; }
#hud {
position: fixed; inset: 0; pointer-events: none;
display: flex; align-items: center; justify-content: center;
font-family: 'Press Start 2P', monospace;
color: #fff; font-size: clamp(0.6rem, 2vw, 1rem);
text-shadow: 2px 2px #000;
opacity: 0; transition: opacity 0.5s;
}
import * as THREE from 'three';
const TOTAL_DEPTH = 80;
const LERP_SPEED = 0.07;
let scrollTarget = 0;
let scrollCurrent = 0;
let scrollProgress = 0;
window.addEventListener('wheel', (e) => {
e.preventDefault();
scrollTarget += e.deltaY * 0.05;
scrollTarget = Math.max(0, Math.min(TOTAL_DEPTH, scrollTarget));
}, { passive: false });
let lastTouchY = 0;
window.addEventListener('touchstart', (e) => { lastTouchY = e.touches[0].clientY; });
window.addEventListener('touchmove', (e) => {
e.preventDefault();
const delta = lastTouchY - e.touches[0].clientY;
lastTouchY = e.touches[0].clientY;
scrollTarget += delta * 0.1;
scrollTarget = Math.max(0, Math.min(TOTAL_DEPTH, scrollTarget));
}, { passive: false });
export function tickScroll(camera, waypoints = []) {
scrollCurrent = scrollCurrent + (scrollTarget - scrollCurrent) * LERP_SPEED;
scrollProgress = scrollCurrent / TOTAL_DEPTH;
camera.position.z = -scrollCurrent;
camera.position.y = Math.sin(scrollCurrent * 0.15) * 0.3;
waypoints.forEach(wp => {
const active = scrollProgress >= wp.from && scrollProgress < wp.to;
wp.onActive?.(active, scrollProgress);
});
}
import * as THREE from 'three';
import { tickScroll } from './scrolly.js';
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: false });
renderer.setSize(innerWidth / 3, innerHeight / 3);
document.getElementById('canvas').style.width = '100vw';
document.getElementById('canvas').style.height = '100vh';
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x000010, 10, 60);
const camera = new THREE.PerspectiveCamera(70, innerWidth / innerHeight, 0.1, 200);
camera.position.set(0, 1, 0);
const waypoints = [
{
from: 0.0, to: 0.25,
onActive: (active) => {
document.getElementById('hud').style.opacity = active ? '1' : '0';
document.getElementById('hud').textContent = active ? 'Chapter I — The Void' : '';
}
},
{
from: 0.5, to: 0.75,
onActive: (active) => {
document.getElementById('hud').style.opacity = active ? '1' : '0';
document.getElementById('hud').textContent = active ? 'Chapter II — The Descent' : '';
}
},
];
renderer.setAnimationLoop(() => {
tickScroll(camera, waypoints);
renderer.render(scene, camera);
});
Patterns de scène pour l'axe Z
const geo = new THREE.BoxGeometry(0.8, 0.8, 0.8);
const mat = new THREE.MeshStandardMaterial();
const mesh = new THREE.InstancedMesh(geo, mat, 400);
const dummy = new THREE.Object3D();
const PALETTE = ['#ff00ff','#00ffff','#ffff00','#7f00ff'];
for (let i = 0; i < 400; i++) {
const angle = i * 0.4;
const radius = 4 + Math.sin(i * 0.3) * 2;
dummy.position.set(
Math.cos(angle) * radius,
Math.sin(angle * 0.5) * 2,
-i * 0.5
);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
mesh.setColorAt(i, new THREE.Color(PALETTE[i % PALETTE.length]));
}
mesh.instanceMatrix.needsUpdate = true;
mesh.instanceColor.needsUpdate = true;
scene.add(mesh);
Progress bar pixel
#progress-bar {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
width: 200px; height: 8px; background: #333; border: 2px solid #fff;
pointer-events: none; z-index: 20; image-rendering: pixelated;
}
#progress-fill {
height: 100%; background: #ff00ff; transition: none;
}
document.getElementById('progress-fill').style.width =
(scrollProgress * 100).toFixed(1) + '%';
Style 2.5D HD-2D — Sprites Pixel Art dans un Monde 3D
L'environnement (sol, bâtiments, terrain) est modélisé en 3D Low-Poly avec des textures
pixel art. Les personnages et objets interactifs sont de purs sprites 2D plats (pixel art)
dressés debout dans le monde 3D via la technique du billboarding.
Principes clés
- Environnement : meshes 3D Low-Poly + textures pixel art (nearest filtering obligatoire)
- Sprites :
PlaneGeometry avec texture PNG transparente orientée toujours vers la caméra
- Caméra : vue plongeante pseudo-isométrique (angle 30-45° sur X)
- Résolution : canvas basse résolution +
image-rendering: pixelated pour unifier le style
- Pas de shadow maps : utiliser des faux ombres (décal ou spot plat sous le sprite)
Setup caméra pseudo-isométrique
const camera = new THREE.PerspectiveCamera(35, innerWidth / innerHeight, 0.1, 500);
camera.position.set(0, 12, 14);
camera.lookAt(0, 0, 0);
camera.rotation.x = -Math.PI / 5;
Texture Pixel Art — Nearest Filtering (OBLIGATOIRE)
const loader = new THREE.TextureLoader();
const tex = loader.load('textures/ground_pixel.png');
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(8, 8);
Billboarding — Sprite 2D dans le monde 3D
import * as THREE from 'three';
export function createBillboard(texturePath, worldScale = 2) {
const loader = new THREE.TextureLoader();
const texture = loader.load(texturePath);
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
const mat = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
alphaTest: 0.1,
depthWrite: false,
side: THREE.DoubleSide,
});
const geo = new THREE.PlaneGeometry(worldScale, worldScale);
const mesh = new THREE.Mesh(geo, mat);
mesh.userData.isBillboard = true;
return mesh;
}
export function updateBillboards(scene, camera) {
scene.traverse((obj) => {
if (obj.userData.isBillboard) {
obj.quaternion.copy(camera.quaternion);
}
});
}
import { createBillboard, updateBillboards } from './sprites.js';
const hero = createBillboard('sprites/hero_idle.png', 2);
hero.position.set(0, 1, 0);
scene.add(hero);
const shadowGeo = new THREE.CircleGeometry(0.4, 8);
const shadowMat = new THREE.MeshBasicMaterial({
color: 0x000000, transparent: true, opacity: 0.4, depthWrite: false
});
const shadow = new THREE.Mesh(shadowGeo, shadowMat);
shadow.rotation.x = -Math.PI / 2;
shadow.position.set(0, 0.01, 0);
scene.add(shadow);
renderer.setAnimationLoop(() => {
updateBillboards(scene, camera);
renderer.render(scene, camera);
});
Animation de spritesheet pixel art
const sheet = loader.load('sprites/hero_walk.png');
sheet.magFilter = THREE.NearestFilter;
sheet.minFilter = THREE.NearestFilter;
sheet.repeat.set(1 / 4, 1);
sheet.offset.set(0, 0);
let frame = 0;
const FRAMES = 4;
const FPS = 8;
let lastFrame = 0;
function updateSpriteFrame(now) {
if (now - lastFrame > 1000 / FPS) {
frame = (frame + 1) % FRAMES;
sheet.offset.x = frame / FRAMES;
lastFrame = now;
}
}
renderer.setAnimationLoop((time) => {
updateSpriteFrame(time);
updateBillboards(scene, camera);
renderer.render(scene, camera);
});
Environnement Low-Poly 3D avec textures pixel
const groundTex = loader.load('textures/grass_pixel.png');
groundTex.magFilter = THREE.NearestFilter;
groundTex.minFilter = THREE.NearestFilter;
groundTex.wrapS = groundTex.wrapT = THREE.RepeatWrapping;
groundTex.repeat.set(16, 16);
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(40, 40),
new THREE.MeshStandardMaterial({ map: groundTex })
);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
const buildingTex = loader.load('textures/wall_pixel.png');
buildingTex.magFilter = THREE.NearestFilter;
buildingTex.minFilter = THREE.NearestFilter;
const building = new THREE.Mesh(
new THREE.BoxGeometry(3, 4, 3),
new THREE.MeshStandardMaterial({ map: buildingTex })
);
building.position.set(6, 2, -4);
scene.add(building);
Effet de parallaxe HD-2D avec couches
const layers = [
{ sprite: createBillboard('sprites/mountain.png', 10), z: -20, parallax: 0.3 },
{ sprite: createBillboard('sprites/tree.png', 3), z: -8, parallax: 0.7 },
{ sprite: createBillboard('sprites/bush.png', 1.5), z: 2, parallax: 1.0 },
];
layers.forEach(l => {
l.sprite.position.z = l.z;
scene.add(l.sprite);
});
function updateParallax(camera) {
layers.forEach(l => {
l.sprite.position.x = camera.position.x * l.parallax;
});
}
Éclairage 2.5D authentique
const ambient = new THREE.AmbientLight(0xfff5e0, 1.2);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xffd080, 1.0);
sun.position.set(8, 20, 5);
scene.add(sun);
Checklist qualité 2.5D