Split View: 웹의 3D 개발 2026 — Three.js·R3F·WebGPU·Gaussian Splatting까지 (현대 3D 웹 스택 심층 가이드)
웹의 3D 개발 2026 — Three.js·R3F·WebGPU·Gaussian Splatting까지 (현대 3D 웹 스택 심층 가이드)
프롤로그 — WebGL은 어른이 되었고, WebGPU는 어제 일이 되었다
2010년대 후반, 웹에서 3D를 한다는 건 "WebGL"을 한다는 뜻이었다. Three.js가 그 위에 사람답게 쓸 수 있는 추상을 얹었고, 우리는 GLSL 셰이더를 두 벌(WebGL용·WebGPU용)로 유지하며 살았다.
2026년 1월, 그 풍경이 완전히 바뀌었다. Safari 26이 macOS Tahoe와 iOS에서 WebGPU를 정식 출시하면서, WebGPU는 Baseline이 되었다. Chrome·Edge·Firefox·Safari 모두 기본으로 켜져 있고, 전역 커버리지는 약 95%다. 나머지 5%는 Three.js의 자동 폴백이 WebGL 2로 처리한다.
이 한 줄로 끝나는 변화가 아니다.
- Three.js r182(2025년 12월 릴리스)가
WebGPURenderer를 권장 렌더러로 올렸다. - TSL(Three Shading Language) — 한 번 짠 셰이더가 WGSL과 GLSL로 동시에 컴파일된다. 두 벌 유지가 끝났다.
- React Three Fiber v9이 비동기
glprop을 받아 WebGPU 초기화를 자연스럽게 처리한다. - Gaussian Splatting이 폴리곤 메시와 나란히 — 폴리곤 없이 — 사진실적 씬을 실시간으로 그리는 새로운 표현이 되었다.
- Meshy·Tripo·Rodin은 텍스트 한 줄로 PBR 텍스처 입은 메시를 뽑는다.
이 글은 2026년의 웹 3D 스택을 한 호흡으로 정리한다. 처음 씬을 띄우는 코드부터 R3F·WebGPU·gsplat·AI 3D까지, 그리고 그 사이에서 "어디서 어떤 도구를 쓸지" 결정하는 매트릭스까지.
1장 · 렌더링 파이프라인 — 3D는 어떻게 그려지는가
먼저 그림 한 장. 도구를 알기 전에, 도구가 무엇을 하는지부터 봐야 한다.
[Scene Graph]
| (mesh / light / camera 트리)
v
[CPU: JS] -- 매트릭스·컬링·정렬 ----+
|
v
[Draw Call]
|
GPU 파이프라인 ──────────────────┴────────────────
| |
v v
Vertex Shader -> Rasterizer -> Fragment Shader
(정점 변환) (픽셀로 자르기) (픽셀 색)
| |
v v
Z-buffer / Blend / Output
|
v
[Framebuffer]
|
v
<canvas>
이 파이프라인의 어디를 누가 책임지는가가 곧 스택 선택의 기준이 된다.
- CPU 쪽 — 씬 그래프, 변환 행렬, 컬링, 정렬. 여기서 끝까지 책임지는 건 Three.js다. R3F는 그 위에 React식 선언으로 얹는다.
- 드로우 콜 — GPU로 던지는 명령 단위. 이게 적을수록 빠르다. 인스턴싱·머지·아틀라스가 다 이걸 줄이는 기술이다.
- 셰이더(Vertex·Fragment) — GPU에서 도는 작은 프로그램. WebGL은 GLSL, WebGPU는 WGSL. TSL이 둘을 하나로 묶는다.
- 출력 합성 — Z-buffer·블렌딩·후처리(post-processing).
기억할 한 줄: "드로우 콜이 모든 것의 절반이고, 셰이더가 나머지 절반이다."
2장 · Three.js — 웹 3D의 사실상 표준
숫자부터. Three.js는 npm 주간 다운로드 270만으로 Babylon.js의 약 270배, PlayCanvas의 약 337배다. "사실상 표준"이 아니라 그냥 표준이다.
가장 작은 씬을 띄우는 코드. 세 가지가 필요하다: 씬·카메라·렌더러, 그리고 안에 들어갈 메시.
import * as THREE from 'three'
// 1. 씬 — 모든 것이 들어가는 컨테이너
const scene = new THREE.Scene()
// 2. 카메라 — 어디서 보는가
const camera = new THREE.PerspectiveCamera(
75, // fov(시야각, degree)
window.innerWidth / window.innerHeight,
0.1, // near clip
1000 // far clip
)
camera.position.z = 5
// 3. 렌더러 — 2026년 기본은 WebGPURenderer
import { WebGPURenderer } from 'three/webgpu'
const renderer = new WebGPURenderer({ antialias: true })
await renderer.init() // 비동기 초기화 — 중요
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// 4. 메시 = Geometry + Material
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({ color: 0x44aa88 })
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
// 5. 라이트 — 표준 머티리얼은 빛이 없으면 검정으로 보인다
scene.add(new THREE.AmbientLight(0xffffff, 0.4))
const dir = new THREE.DirectionalLight(0xffffff, 1.0)
dir.position.set(5, 10, 7.5)
scene.add(dir)
// 6. 루프 — requestAnimationFrame이 아니라 setAnimationLoop
renderer.setAnimationLoop(() => {
cube.rotation.x += 0.01
cube.rotation.y += 0.01
renderer.render(scene, camera)
})
세 가지를 짚는다.
await renderer.init()— WebGPU는 비동기다. 이 한 줄을 잊으면 첫 프레임이 검정이다.MeshStandardMaterial은 라이트가 필요하다 — 화면이 검정이면 라이트부터 의심한다.setAnimationLoop—requestAnimationFrame대신. WebXR이 자동으로 잡힌다.
이게 모든 Three.js 코드의 뼈대다. 나머지는 이 위에 얹는 것이다.
3장 · React Three Fiber + drei — React식 3D
명령형 코드는 작을 때는 깔끔하지만, 씬이 50개 노드를 넘어가면 빠르게 누더기가 된다. React Three Fiber(R3F) 는 Three.js를 React 컴포넌트 트리로 표현한다.
같은 큐브를 R3F로 다시 쓰면:
import { Canvas } from '@react-three/fiber'
import { OrbitControls, Environment } from '@react-three/drei'
function Cube() {
return (
<mesh rotation={[0, 0.4, 0]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
)
}
export default function Scene() {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 75 }}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 10, 7.5]} intensity={1} />
<Cube />
<OrbitControls />
<Environment preset="city" />
</Canvas>
)
}
같은 일을 한다. 그런데:
- 씬 그래프가 React 트리다. 조건부 렌더링·상태 관리·Hooks가 그대로 통한다.
<Canvas>는 리사이즈·렌더 루프·픽셀 비율을 알아서 잡는다.- drei — Poimandres 팀의 헬퍼 라이브러리.
OrbitControls·Environment·useGLTF·Html·Text같은 매일 쓰는 것들이 다 있다.
R3F v9의 WebGPU — 비동기 gl prop
R3F v9이 gl prop을 비동기 팩토리로 받을 수 있게 됐다. WebGPU 초기화가 자연스럽게 묶인다.
import { Canvas } from '@react-three/fiber'
import { WebGPURenderer } from 'three/webgpu'
<Canvas
gl={async (props) => {
const renderer = new WebGPURenderer(props)
await renderer.init()
return renderer
}}
>
{/* ...씬... */}
</Canvas>
2026년 5월 현재, R3F의 WebGPU 통합은 아직 완전히 매끄럽지는 않지만 — Poimandres 팀이 적극적으로 다듬는 중이고 — 위 패턴은 프로덕션에서 충분히 돈다. WebGL 2 폴백이 필요하면 WebGLRenderer를 던지면 된다.
useFrame — 매 프레임 훅
R3F의 가장 React스러운 부분. 컴포넌트가 매 프레임 호출되는 콜백을 등록한다.
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
function Spinner() {
const ref = useRef(null)
useFrame((state, delta) => {
if (ref.current) ref.current.rotation.y += delta
})
return (
<mesh ref={ref}>
<torusKnotGeometry args={[1, 0.3, 128, 32]} />
<meshStandardMaterial color="orange" />
</mesh>
)
}
delta는 이전 프레임으로부터의 초 단위 경과. 프레임률에 안 흔들리는 애니메이션의 출발점.
4장 · WebGPU와 TSL — 셰이더 두 벌이 한 벌로
WebGL과 WebGPU의 차이를 한 문장: WebGL은 OpenGL ES 2.0의 웹 포팅, WebGPU는 Vulkan·Metal·DX12 시대의 현대적 GPU API.
실무적으로 무엇이 달라지는가:
- 드로우 콜 비용이 낮다 — 드로우 콜이 많은 씬(파티클·인스턴스 다수)에서 2~10배 빨라진다.
- 컴퓨트 셰이더가 1급 시민 — GPGPU(파티클·물리·시뮬레이션·포스트프로세싱)가 메인 파이프라인 안에서 자연스럽다.
- WGSL — WebGL의 GLSL 대신 새로운 셰이더 언어.
마지막이 항상 골치였다. WebGL 시대에 GLSL을 짰는데, WebGPU로 가려면 WGSL로 다시 써야 했다. TSL이 이걸 끝낸다.
TSL = Three Shading Language. 노드 기반 셰이더 추상. 한 번 짜면 Three.js가 내부적으로 WGSL(WebGPU용)·GLSL(WebGL용) 양쪽으로 컴파일한다.
간단한 노이즈 셰이더(머티리얼 색을 노이즈로 흔드는 예):
import { MeshStandardNodeMaterial } from 'three/webgpu'
import { uniform, vec3, mix, sin, time, positionLocal } from 'three/tsl'
const speed = uniform(1.0)
const wave = sin(positionLocal.y.mul(8.0).add(time.mul(speed)))
const color = mix(vec3(0.1, 0.4, 0.9), vec3(1.0, 0.4, 0.2), wave.mul(0.5).add(0.5))
const material = new MeshStandardNodeMaterial()
material.colorNode = color
positionLocal·time·mix·sin 다 노드다. JS로 셰이더를 조립한다. GLSL 텍스트도 WGSL 텍스트도 손으로 안 짠다.
작은 WGSL 한 조각이 어떻게 생겼는지만 참고로 (TSL이 내부에서 비슷한 걸 만든다):
@fragment
fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
let c = mix(vec3(0.1, 0.4, 0.9), vec3(1.0, 0.4, 0.2), sin(uv.y * 8.0) * 0.5 + 0.5);
return vec4(c, 1.0);
}
요는 — 2026년에는 직접 짤 일이 거의 없다. TSL이 처리한다. WGSL을 한 번 읽어두는 정도면 충분하다.
5장 · glTF — 3D의 JPEG
폴리곤 모델을 웹으로 옮기는 표준은 glTF 2.0이다. "3D의 JPEG"라고 부른다. PBR 머티리얼·애니메이션·스킨·드라코 압축까지 한 파일에 들어간다.
Three.js의 로더:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
const draco = new DRACOLoader()
draco.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/')
const loader = new GLTFLoader()
loader.setDRACOLoader(draco)
loader.load('/models/robot.glb', (gltf) => {
scene.add(gltf.scene)
// gltf.animations 에는 AnimationClip 배열이 들어있다
})
R3F + drei로는 한 줄이다:
import { useGLTF } from '@react-three/drei'
function Robot() {
const { scene } = useGLTF('/models/robot.glb')
return <primitive object={scene} />
}
useGLTF.preload('/models/robot.glb')
useGLTF는 Suspense로 묶이고, 같은 모델을 여러 곳에서 쓰면 한 번만 로드한다. preload로 미리 받아둘 수도 있다.
glTF 최적화 — 3가지만 기억
- Draco 압축 — 정점 데이터를 압축. 파일 크기 5~10배 작아진다.
- KTX2 / Basis 텍스처 — JPG·PNG 대신 GPU가 바로 먹는 압축 텍스처. 메모리·로드 시간 절약.
gltf-transformCLI — 위 둘을 한 번에 해주는 도구. CI에 박아두면 더 이상 신경 안 써도 된다.
npx @gltf-transform/cli optimize input.glb output.glb \
--texture-compress webp --simplify 0.5
6장 · 애니메이션 — 클립부터 스프링까지
3D 애니메이션은 크게 세 갈래다.
- glTF에 들어있는 본/스킨 애니메이션 — Blender·Maya에서 만든 걸 그대로 재생.
- 수학으로 도는 애니메이션 —
useFrame안에서 회전·트랜슬레이션. - 인터랙션 기반 — 호버·드래그·스크롤에 반응. 보통 스프링 물리로.
glTF 클립 재생
import { useAnimations, useGLTF } from '@react-three/drei'
import { useEffect } from 'react'
function Robot() {
const { scene, animations } = useGLTF('/models/robot.glb')
const { actions, names } = useAnimations(animations, scene)
useEffect(() => {
actions[names[0]]?.reset().fadeIn(0.3).play()
}, [actions, names])
return <primitive object={scene} />
}
스프링 애니메이션 — react-spring/three
import { useSpring, animated } from '@react-spring/three'
function Box({ hovered }) {
const { scale } = useSpring({ scale: hovered ? 1.3 : 1.0 })
return (
<animated.mesh scale={scale}>
<boxGeometry />
<meshStandardMaterial />
</animated.mesh>
)
}
값이 튀지 않고 물리적으로 자연스럽게 보간된다. 포트폴리오·랜딩 페이지의 미세한 디테일에 결정적이다.
7장 · 포스트프로세싱 — 한 끗 차이의 마감
같은 씬이라도 블룸·SSAO·필름 그레인 한 번 거치면 영상미가 다른 차원이 된다. 표준 라이브러리는 postprocessing (Vanruesc), R3F 래퍼는 @react-three/postprocessing.
import { EffectComposer, Bloom, DepthOfField, Vignette } from '@react-three/postprocessing'
<Canvas>
{/* ...씬... */}
<EffectComposer>
<Bloom intensity={1.2} luminanceThreshold={0.6} mipmapBlur />
<DepthOfField focusDistance={0} focalLength={0.02} bokehScale={2} />
<Vignette eskil={false} offset={0.1} darkness={1.0} />
</EffectComposer>
</Canvas>
조심할 점: 포스트프로세싱은 풀스크린 패스다 — 픽셀이 많을수록 비싸다. 모바일에서는 항상 pixelRatio를 캡(보통 1.5~2.0)하고, 효과 두세 개만 고른다.
8장 · 성능 — 드로우 콜이 모든 것의 절반
3D 웹 성능은 거의 항상 드로우 콜 수와 셰이더 비용 두 곳에서 갈린다. 2026년의 핵심 패턴 다섯.
1. 인스턴싱 — 같은 메시 1만 개를 한 번에
같은 지오메트리·머티리얼의 메시를 여러 개 그려야 할 때(나무·풀·박스 더미), 인스턴싱을 쓰면 드로우 콜이 1로 줄어든다.
import { Instances, Instance } from '@react-three/drei'
<Instances limit={10000}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="white" />
{positions.map((p, i) => (
<Instance key={i} position={p} />
))}
</Instances>
WebGPU는 인스턴싱 비용이 더 낮다. WebGL에서 5천 인스턴스가 한계였다면, WebGPU에서는 5만이 종종 돈다.
2. 프러스텀 컬링·LOD
Three.js는 기본으로 카메라 시야 밖 객체를 그리지 않는다(프러스텀 컬링). 단, Mesh.frustumCulled는 기본 true니까 끄지 말 것. LOD(Level of Detail)는 카메라 거리에 따라 메시 해상도를 바꾼다.
import { Detailed } from '@react-three/drei'
<Detailed distances={[0, 10, 50]}>
<HighPolyMesh />
<MidPolyMesh />
<LowPolyMesh />
</Detailed>
3. 머티리얼·지오메트리 공유
같은 머티리얼·지오메트리는 메모리에서 단 한 벌만. R3F에서는 컴포넌트 바깥에서 만들어 공유한다.
4. 텍스처 — KTX2와 mipmap
JPG·PNG는 CPU에서 디코드 후 GPU로 업로드. KTX2(Basis Universal)는 GPU가 압축 그대로 먹는다. 로드는 빠르고, VRAM도 적게 쓴다.
5. pixelRatio 캡
레티나에서 devicePixelRatio가 3이면 화면 픽셀이 9배다. 항상 캡한다.
<Canvas dpr={[1, 2]}> {/* 최소 1, 최대 2 */}
9장 · WebXR — VR·AR을 웹으로
setAnimationLoop 한 줄과 WebXRManager 덕에 Three.js의 WebXR은 거의 공짜다. R3F에는 @react-three/xr이 있다.
import { XR, createXRStore, XROrigin } from '@react-three/xr'
const store = createXRStore()
<button onClick={() => store.enterVR()}>VR 진입</button>
<Canvas>
<XR store={store}>
<XROrigin />
{/* ...씬... */}
</XR>
</Canvas>
WebGPU + WebXR 조합은 2026년에 의외로 무겁지 않다 — Apple Vision Pro·Quest 3·Quest 3S 모두 WebGPU 기반 WebXR을 안정적으로 돌린다. 마케팅·교육·헬스케어 쪽에서 빠르게 채택 중.
10장 · Three.js vs Babylon.js vs PlayCanvas
엔진 비교는 짧게.
| 항목 | Three.js | Babylon.js | PlayCanvas |
|---|---|---|---|
| 라이선스 | MIT | Apache 2.0 | MIT (엔진) |
| 강점 | 거대한 생태계·예제·커뮤니티 | 게임 기능 풍부(피직스·오디오·머티리얼 에디터) | 비주얼 에디터·클라우드 IDE |
| 약점 | 게임용 기능은 직접 조립 | 생태계 작음 | 코드 우선은 약함 |
| WebGPU | r182 기본 권장 | Babylon 7+에서 안정 | 엔진 차원에서 지원 |
| 주간 다운로드(npm) | ~2.7M | ~10K | ~8K |
| 대표 영역 | 포트폴리오·제품·아트·시각화 | 브라우저 게임·시뮬레이션 | 광고·게임·구성 도구 |
한 줄 추천:
- 창작·아트·포트폴리오·제품 시각화 → Three.js (+ R3F).
- 게임형 인터랙션·물리 비중 큰 앱 → Babylon.js.
- 에디터로 시각적 협업이 중요 → PlayCanvas.
생태계 크기가 깡패다. 모르면 Three.js다.
11장 · Gaussian Splatting — 폴리곤 없는 사진실적 3D
여기서부터 새로운 페이지다.
Gaussian Splatting(이하 gsplat) 은 폴리곤 메시가 아니다. 씬을 수백만 개의 작은 3D 가우시안 점들(각자 위치·색·투명도·방향성을 가진 타원체)로 표현한다. 카메라가 그 점들을 화면에 "스플랫"(투영해서 펴)으로써 이미지를 만든다.
폴리곤 메시: Gaussian Splatting:
┌── 정점·면 데이터 ┌── 수백만 개 가우시안
├── UV·텍스처 │ (위치·SH 색·스케일·회전·α)
├── 노멀·머티리얼 ├── 텍스처 없음
└── 라이트로 셰이딩 └── 캡처 당시의 라이팅이 굽혀짐
왜 흥분되는가?
- 사진실적 — 30장~수백 장의 사진/영상에서 학습. 출력이 사진과 거의 같다.
- 실시간 — GPU 친화적. 웹에서 60fps 가능.
- 메시 모델링 0 — Blender도, UV도, 텍스처도, 노멀도 필요 없다. 카메라만 있으면 된다.
- NeRF의 후계 — NeRF가 학술적 마일스톤이었다면, gsplat은 실무적 도구다(빠른 학습·실시간 렌더링).
한계도 분명하다
- 라이팅이 "구워져" 있다 — 씬을 동적으로 비추기 어렵다.
- 충돌·물리 시뮬레이션이 어렵다 — 메시가 아니니까.
- 편집이 까다롭다 — SuperSplat 같은 전용 에디터가 필요.
- 파일이 크다 — 수십~수백 MB.
요는 — 존재하는 공간을 통째로 캡처해서 보여주는 것에는 무적이다. 부동산·문화재·박물관·콘서트·이벤트·전시.
2026년의 도구 풍경
- Polycam — 모바일 캡처의 시장 선도자. iOS LiDAR + 포토그래메트리 + gsplat. 평균 평점 4.7. iOS 리뷰 54만 개. 가장 쉬운 진입.
- Luma AI — 클라우드 처리로 시각 품질이 가장 좋다고 평가되는 무료 gsplat 플랫폼. 임베드 가능.
- SuperSplat — PlayCanvas 엔진 위에 만든 무료 오픈소스 브라우저 기반 gsplat 에디터. 라이브 어노테이션·핫스팟·후처리(블룸·비네트)·카메라 애니메이션·WebXR까지. HTML 뷰어로 내보내 GitHub Pages·Netlify·Vercel에 그대로 호스팅.
- NeRF Studio — 연구 지향. 로컬 학습·실험에 강함.
웹에 띄우기 — @mkkellogg/gaussian-splats-3d
Three.js 호환의 가벼운 gsplat 뷰어. R3F 환경에서:
import { GaussianSplats3D } from '@mkkellogg/gaussian-splats-3d'
import { useThree } from '@react-three/fiber'
import { useEffect } from 'react'
function Splat({ url }) {
const { scene, camera, gl } = useThree()
useEffect(() => {
const viewer = new GaussianSplats3D.Viewer({
threeScene: scene,
camera,
renderer: gl,
selfDrivenMode: false,
})
viewer.addSplatScene(url)
return () => viewer.dispose()
}, [url, scene, camera, gl])
return null
}
브라우저에서 수백만 가우시안을 60fps로 그린다. 5년 전에는 SF였다.
12장 · AI로 3D를 만든다 — Meshy · Tripo · Rodin
마지막 한 갈래. 텍스트나 사진에서 3D 메시를 뽑는 AI. 2026년에는 더 이상 실험이 아니라 워크플로의 일부다.
세 강자.
- Meshy 6 — 가장 균형 잡힌 제품. 텍스트→3D, 이미지→3D, PBR 텍스처, 토폴로지 제어, 폭넓은 익스포트. 생성 40~60초. "기본값 추천."
- Tripo AI — 가장 빠른 생성(20~30초). 기본값이 영리해서 초기 진입 마찰이 가장 낮다. 텍스트·이미지 둘 다.
- Rodin AI(Gen-2) — 100억 파라미터. 가장 높은 품질. 캐릭터·구조화된 자산에 강함. 생성 60~180초.
워크플로 예:
- 컨셉 단계 — Tripo에서 빠른 변형 30초.
- 마음에 드는 안 — Meshy에서 PBR 텍스처 깔끔하게 다시.
- 최종 캐릭터 — Rodin Gen-2로 고품질 메시.
- glTF로 내보내 Three.js/R3F 씬에 박는다.
Idea ─▶ Tripo (탐색)
└─▶ Meshy (정제·PBR)
└─▶ Rodin (피니시·캐릭터)
└─▶ glTF
└─▶ R3F 씬에 useGLTF
중요한 현실: AI 생성 메시는 토폴로지가 깔끔하지 않다. 포트폴리오·시각화·게임 백그라운드에는 충분하지만, 리깅·애니메이션이 중요한 캐릭터는 보통 Blender에서 리토폴로지가 필요하다.
13장 · "무엇으로 무엇을 만들까" — 유스케이스 매트릭스
| 유스케이스 | 추천 스택 | 비고 |
|---|---|---|
| 개발자 포트폴리오 | R3F + drei + Bloom | drei의 Float/Text 한 줌으로 충분 |
| 제품 컨피규레이터 | R3F + glTF + KTX2 | 색·텍스처 옵션은 머티리얼 교체 |
| 부동산 가상 투어 | gsplat (Luma·Polycam) + SuperSplat | 사진실적·실측 공간 |
| 박물관·전시 | gsplat + WebXR | 핫스팟·어노테이션 |
| 브라우저 게임 | Babylon.js 또는 Three.js + Rapier | 물리·충돌 필요 |
| 데이터 시각화 | R3F + 카메라 워크 | 인스턴싱 적극 활용 |
| AR 마케팅 | R3F + @react-three/xr + WebXR | iOS Quick Look 병행 |
| 인터랙티브 아트 | Three.js + TSL (셰이더 직접) | 노드 셰이더 자유도 |
| 캐릭터 중심 인터랙션 | R3F + AI 생성(Rodin) + Mixamo 리타게팅 | 토폴로지 정리 한 번 |
| LiDAR 캡처 자산 | Polycam → glTF or gsplat | 모바일만으로 종결 |
룰 오브 섬:
- "공간을 통째로 보여줘야 한다" → gsplat.
- "조작할 수 있어야 한다(색·옵션·물리)" → 폴리곤(glTF) + R3F.
- "조금 만들어 빨리 띄워야 한다" → R3F + AI 생성.
14장 · 포트폴리오 사이트 만들기 — 30분 레시피
가장 흔한 첫 프로젝트. 골격을 한 번에 본다.
import { Canvas } from '@react-three/fiber'
import { OrbitControls, Environment, Float, Text3D, useGLTF, ContactShadows } from '@react-three/drei'
import { EffectComposer, Bloom } from '@react-three/postprocessing'
import { Suspense } from 'react'
function Hero() {
const { scene } = useGLTF('/hero.glb')
return <primitive object={scene} scale={1.4} />
}
export default function Portfolio() {
return (
<Canvas camera={{ position: [0, 0, 6], fov: 50 }} dpr={[1, 2]}>
<color attach="background" args={['#0a0a0a']} />
<Suspense fallback={null}>
<Environment preset="studio" />
<Float speed={1.5} rotationIntensity={0.4} floatIntensity={0.8}>
<Hero />
</Float>
<ContactShadows position={[0, -1.6, 0]} opacity={0.6} blur={2.4} />
</Suspense>
<OrbitControls enableZoom={false} />
<EffectComposer>
<Bloom intensity={0.8} mipmapBlur />
</EffectComposer>
</Canvas>
)
}
체크리스트:
- 모델은 Meshy/Tripo로 뽑거나, sketchfab CC0에서 받아
gltf-transform optimize한 번. - 배경은 단색 + Environment HDR 한 장(스튜디오/시티).
- Float·ContactShadows·Bloom으로 "AI 생성스러움"을 가린다.
dpr={[1, 2]}로 레티나 폭주 막기.- 모바일에서는 Bloom을 끈다(미디어 쿼리 + 조건부 렌더).
에필로그 — 웹은 진짜로 3D가 되었다
2026년의 웹 3D는 더 이상 "한번 보고 마는 데모"의 자리가 아니다. 제품 페이지, 부동산, 박물관, 광고, 학습 도구가 일상적으로 3D를 깔고 있다. 그리고 그 일상의 도구는:
- Three.js + R3F — 폴리곤 기반 표준 스택.
- WebGPU + TSL — 셰이더 두 벌이 한 벌로.
- Gaussian Splatting — 카메라만 있으면 사진실적 공간.
- AI 3D 생성 — 텍스트 한 줄로 메시.
마지막으로 두 가지를 남긴다.
14개 항목 체크리스트
- WebGPURenderer를 기본으로 두고 WebGL 2 폴백을 확인했는가?
renderer.init()의 비동기 처리를 빠뜨리지 않았는가?- glTF가 Draco·KTX2로 압축됐는가?
- 인스턴싱이 필요한 대량 메시를 그냥 그리고 있지 않은가?
pixelRatio를 모바일에서 캡했는가?- 프러스텀 컬링을 끄지 않았는가?
- 같은 머티리얼/지오메트리를 중복 생성하고 있지 않은가?
- 포스트프로세싱 효과 수를 모바일에서 줄였는가?
- Suspense로 로딩 UX를 잡았는가?
- WebXR 진입은 사용자 제스처(클릭) 안에서 호출하는가?
- gsplat 자산은 압축 포맷(SPZ·KSPLAT)으로 내보냈는가?
- AI 생성 메시의 토폴로지를 (필요하면) 한 번 정리했는가?
setAnimationLoop한 곳에서만 도는가(중복 루프 없음)?- 첫 프레임 검정이 라이트 없음/init 누락은 아닌가?
안티패턴 10가지
- WebGPU 코드에
await renderer.init()을 빼고 첫 프레임이 검정. - 라이트 없이
MeshStandardMaterial을 쓰고 검정. - 같은 모델 인스턴스를 매 프레임 다시 로드.
frustumCulled = false를 그냥 켜둠.- JPG·PNG를 압축 없이 그대로 GPU에.
- 데스크톱·모바일 같은
dpr설정. - 포스트프로세싱 5개를 모바일에서도 돌림.
- AI 생성 메시를 토폴로지 정리 없이 캐릭터 리깅에 쓰기.
- gsplat을 폴리곤 워크플로(편집·물리)로 다루려 함.
useGLTF대신 매 컴포넌트에서GLTFLoader.load를 직접 호출.
다음 글 예고
다음 글 후보: WebGPU 컴퓨트 셰이더 실전 — GPGPU로 파티클 100만 개 돌리기, Gaussian Splatting 워크플로 — 캡처부터 웹 임베드까지, R3F + Rapier 물리 엔진 — 인터랙티브 3D 게임 한 시간 만에.
"웹은 진짜로 3D가 되었다. 폴리곤은 메시로, 사진은 가우시안으로, 텍스트는 AI로. 그 사이를 잇는 건 여전히 Three.js다."
— 웹의 3D 개발 2026, 끝.
참고 / References
- Three.js 공식 사이트
- Three.js WebGPURenderer 매뉴얼
- Three.js GitHub 릴리스 노트
- React Three Fiber 문서
- drei 헬퍼 라이브러리
- @react-three/postprocessing
- @react-three/xr
- WebGPU W3C 명세
- WGSL 명세
- TSL — Three Shading Language 가이드
- Babylon.js 공식
- PlayCanvas 공식
- SuperSplat — gsplat 에디터
- Polycam — 모바일 3D 캡처
- Luma AI
- NeRF Studio
- Meshy AI
- Tripo AI
- Rodin AI (Hyper3D)
- gltf-transform — 최적화 CLI
- Mkkellogg gaussian-splats-3d
3D Development for the Web in 2026 — Three.js, R3F, WebGPU, Gaussian Splatting (a deep-dive on the modern web 3D stack) (english)
Prologue — WebGL grew up, and WebGPU is already yesterday
In the late 2010s, "doing 3D on the web" meant "doing WebGL." Three.js stacked a humane abstraction on top, and we lived with two copies of every shader — GLSL for WebGL today, WGSL for WebGPU tomorrow.
January 2026 closed that chapter. With Safari 26 shipping WebGPU on macOS Tahoe and iOS, WebGPU became Baseline. Chrome, Edge, Firefox, Safari — all on by default, with global coverage around 95%. The remaining 5% silently falls back to WebGL 2 through Three.js.
That sentence is not the whole change.
- Three.js r182 (December 2025) made
WebGPURendererthe recommended renderer. - TSL (Three Shading Language) — write the shader once; Three.js compiles to both WGSL and GLSL. No more dual maintenance.
- React Three Fiber v9 accepts an async
glprop, so WebGPU init wires up cleanly. - Gaussian Splatting is now a real, distinct way of representing scenes — photorealistic, real-time, polygon-free.
- Meshy / Tripo / Rodin spit out PBR-textured meshes from a single sentence.
This post walks the 2026 web-3D stack end-to-end. From the first scene on screen to R3F, WebGPU, gsplat, AI 3D — and a matrix in the middle telling you which tool to pick for which use case.
1. The rendering pipeline — how 3D actually gets drawn
A diagram first. You can't compare tools until you know what tools do.
[Scene Graph]
| (tree of mesh / light / camera)
v
[CPU: JS] -- matrices / culling / sorting --+
|
v
[Draw Call]
|
GPU pipeline ────────────────────────────┴──────────────
| |
v v
Vertex Shader -> Rasterizer -> Fragment Shader
(transform verts) (slice to px) (color the px)
| |
v v
Z-buffer / Blend / Output
|
v
[Framebuffer]
|
v
\<canvas\>
Where this pipeline lives matters because it tells you what each library actually owns.
- CPU side — scene graph, transform matrices, culling, sorting. Three.js owns this end-to-end. R3F lays a declarative React layer on top.
- Draw calls — units of work sent to the GPU. Fewer is faster. Instancing, merging, atlasing all exist to shrink this number.
- Shaders (vertex / fragment) — tiny GPU programs. WebGL = GLSL, WebGPU = WGSL. TSL unifies the two.
- Output compositing — Z-buffer, blending, post-processing.
One line to remember: "Half of performance is draw calls; the other half is shaders."
2. Three.js — the de-facto standard for web 3D
By the numbers: Three.js has roughly 2.7 million weekly npm downloads, ~270x Babylon.js and ~337x PlayCanvas. It is not the "de facto standard" — it is the standard.
The smallest scene needs three things — scene, camera, renderer — plus a mesh inside.
import * as THREE from 'three'
// 1. Scene — the container for everything
const scene = new THREE.Scene()
// 2. Camera — where you look from
const camera = new THREE.PerspectiveCamera(
75, // fov (degrees)
window.innerWidth / window.innerHeight,
0.1, // near clip
1000 // far clip
)
camera.position.z = 5
// 3. Renderer — in 2026, WebGPURenderer is the default
import { WebGPURenderer } from 'three/webgpu'
const renderer = new WebGPURenderer({ antialias: true })
await renderer.init() // async init — required
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// 4. Mesh = Geometry + Material
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({ color: 0x44aa88 })
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
// 5. Lights — Standard materials need light or they render black
scene.add(new THREE.AmbientLight(0xffffff, 0.4))
const dir = new THREE.DirectionalLight(0xffffff, 1.0)
dir.position.set(5, 10, 7.5)
scene.add(dir)
// 6. Loop — use setAnimationLoop (not requestAnimationFrame)
renderer.setAnimationLoop(() => {
cube.rotation.x += 0.01
cube.rotation.y += 0.01
renderer.render(scene, camera)
})
Three things to call out.
await renderer.init()— WebGPU is async. Skip this and your first frame is black.MeshStandardMaterialneeds light — black screen? Suspect lighting first.setAnimationLoop— notrequestAnimationFrame. It hooks WebXR for free.
That snippet is the skeleton of every Three.js program. Everything else is laid on top.
3. React Three Fiber + drei — the React way to do 3D
Imperative Three.js is fine when small, but once your scene has 50 nodes it turns into spaghetti. React Three Fiber (R3F) maps Three.js onto a React component tree.
Same cube in R3F:
import { Canvas } from '@react-three/fiber'
import { OrbitControls, Environment } from '@react-three/drei'
function Cube() {
return (
<mesh rotation={[0, 0.4, 0]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
)
}
export default function Scene() {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 75 }}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 10, 7.5]} intensity={1} />
<Cube />
<OrbitControls />
<Environment preset="city" />
</Canvas>
)
}
Same outcome. But:
- The scene graph is a React tree. Conditional rendering, state, hooks all just work.
<Canvas>handles resize, the render loop, and pixel ratio for you.- drei — the Poimandres helper library.
OrbitControls,Environment,useGLTF,Html,Text— all the daily-driver utilities live there.
R3F v9 + WebGPU — async gl prop
R3F v9 lets the gl prop be an async factory. WebGPU's async init slots in naturally.
import { Canvas } from '@react-three/fiber'
import { WebGPURenderer } from 'three/webgpu'
<Canvas
gl={async (props) => {
const renderer = new WebGPURenderer(props)
await renderer.init()
return renderer
}}
>
{/* ...scene... */}
</Canvas>
As of May 2026, R3F's WebGPU story is still smoothing out — Poimandres is actively polishing — but the pattern above is production-viable. Want WebGL 2 fallback? Pass WebGLRenderer instead.
useFrame — the per-frame hook
The most React-flavored part of R3F. A component registers a callback fired every frame.
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
function Spinner() {
const ref = useRef(null)
useFrame((state, delta) => {
if (ref.current) ref.current.rotation.y += delta
})
return (
<mesh ref={ref}>
<torusKnotGeometry args={[1, 0.3, 128, 32]} />
<meshStandardMaterial color="orange" />
</mesh>
)
}
delta is the seconds elapsed since the previous frame. That is the starting point of every framerate-independent animation.
4. WebGPU and TSL — two shader copies become one
One-line difference: WebGL is the web port of OpenGL ES 2.0; WebGPU is a modern GPU API in the Vulkan / Metal / DX12 family.
What this means in practice:
- Lower per-draw-call cost — in draw-call-heavy scenes (particles, many instances), expect 2–10x.
- Compute shaders as first-class citizens — GPGPU (particles, physics, sims, post) sits naturally in the main pipeline.
- WGSL — a new shading language, replacing GLSL for WebGPU.
The last one used to be a real headache. GLSL written for WebGL had to be re-authored in WGSL for WebGPU. TSL ends that.
TSL = Three Shading Language. A node-based shader abstraction. Write once; Three.js compiles it to both WGSL (WebGPU) and GLSL (WebGL) internally.
A simple noise shader (wobble the material color by a sine wave):
import { MeshStandardNodeMaterial } from 'three/webgpu'
import { uniform, vec3, mix, sin, time, positionLocal } from 'three/tsl'
const speed = uniform(1.0)
const wave = sin(positionLocal.y.mul(8.0).add(time.mul(speed)))
const color = mix(vec3(0.1, 0.4, 0.9), vec3(1.0, 0.4, 0.2), wave.mul(0.5).add(0.5))
const material = new MeshStandardNodeMaterial()
material.colorNode = color
positionLocal, time, mix, sin are all nodes. You assemble shaders in JavaScript. You write neither GLSL nor WGSL by hand.
A small WGSL fragment for reference (TSL emits something like this under the hood):
@fragment
fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
let c = mix(vec3(0.1, 0.4, 0.9), vec3(1.0, 0.4, 0.2), sin(uv.y * 8.0) * 0.5 + 0.5);
return vec4(c, 1.0);
}
The point is — in 2026 you almost never write either by hand. TSL handles it. Reading WGSL once for literacy is enough.
5. glTF — the JPEG of 3D
The standard for shipping polygon models over the web is glTF 2.0. People call it "the JPEG of 3D." PBR materials, animations, skinning, Draco compression — all in one file.
Three.js loader:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
const draco = new DRACOLoader()
draco.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/')
const loader = new GLTFLoader()
loader.setDRACOLoader(draco)
loader.load('/models/robot.glb', (gltf) => {
scene.add(gltf.scene)
// gltf.animations is an array of AnimationClips
})
R3F + drei collapse that to one line:
import { useGLTF } from '@react-three/drei'
function Robot() {
const { scene } = useGLTF('/models/robot.glb')
return <primitive object={scene} />
}
useGLTF.preload('/models/robot.glb')
useGLTF is Suspense-aware; the same model used in multiple places loads once. preload lets you hit the network early.
glTF optimization — three things
- Draco compression — compresses vertex data. Files shrink 5–10x.
- KTX2 / Basis textures — instead of JPG/PNG, GPU-native compressed textures. Lower memory, faster load.
gltf-transformCLI — applies both with one command. Bake it into CI and forget.
npx @gltf-transform/cli optimize input.glb output.glb \
--texture-compress webp --simplify 0.5
6. Animation — from clips to springs
Three streams of 3D animation:
- Bone / skin animations baked into glTF — straight playback of what Blender or Maya produced.
- Math-driven — rotate, translate, scale inside
useFrame. - Interaction-driven — hover, drag, scroll. Usually spring physics.
Playing glTF clips
import { useAnimations, useGLTF } from '@react-three/drei'
import { useEffect } from 'react'
function Robot() {
const { scene, animations } = useGLTF('/models/robot.glb')
const { actions, names } = useAnimations(animations, scene)
useEffect(() => {
actions[names[0]]?.reset().fadeIn(0.3).play()
}, [actions, names])
return <primitive object={scene} />
}
Spring animation — react-spring/three
import { useSpring, animated } from '@react-spring/three'
function Box({ hovered }) {
const { scale } = useSpring({ scale: hovered ? 1.3 : 1.0 })
return (
<animated.mesh scale={scale}>
<boxGeometry />
<meshStandardMaterial />
</animated.mesh>
)
}
Values don't jerk — they interpolate the way real objects do. It's the difference between "AI-generated feel" and "polished portfolio."
7. Post-processing — the half-step that defines the finish
The same scene, run through Bloom, SSAO, film grain, lives in a different visual universe. The standard library is postprocessing (Vanruesc); R3F's wrapper is @react-three/postprocessing.
import { EffectComposer, Bloom, DepthOfField, Vignette } from '@react-three/postprocessing'
<Canvas>
{/* ...scene... */}
<EffectComposer>
<Bloom intensity={1.2} luminanceThreshold={0.6} mipmapBlur />
<DepthOfField focusDistance={0} focalLength={0.02} bokehScale={2} />
<Vignette eskil={false} offset={0.1} darkness={1.0} />
</EffectComposer>
</Canvas>
Caveat: post-processing is full-screen — cost scales with pixel count. On mobile, cap pixelRatio (1.5–2.0) and pick two or three effects max.
8. Performance — draw calls are half of everything
3D web performance almost always comes down to draw-call count and shader cost. The 2026 playbook in five patterns.
1. Instancing — 10,000 of the same mesh in one draw call
For many copies of the same geometry / material (trees, grass, box piles), instancing collapses draw calls to 1.
import { Instances, Instance } from '@react-three/drei'
<Instances limit={10000}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="white" />
{positions.map((p, i) => (
<Instance key={i} position={p} />
))}
</Instances>
Instancing is cheaper on WebGPU. If 5,000 instances was the WebGL ceiling, 50,000 is often fine on WebGPU.
2. Frustum culling, LOD
Three.js culls objects outside the camera by default. Mesh.frustumCulled is true out of the box — don't turn it off. LOD swaps mesh resolution by camera distance.
import { Detailed } from '@react-three/drei'
<Detailed distances={[0, 10, 50]}>
<HighPolyMesh />
<MidPolyMesh />
<LowPolyMesh />
</Detailed>
3. Share materials and geometries
Same material / geometry should exist once in memory. In R3F, create them outside the component and reuse.
4. Textures — KTX2 and mipmaps
JPG/PNG: decoded by the CPU, then uploaded. KTX2 (Basis Universal): the GPU consumes the compressed bytes directly. Faster load, lower VRAM.
5. Cap pixelRatio
A retina device with devicePixelRatio = 3 shades 9x the pixels. Always cap.
<Canvas dpr={[1, 2]}> {/* min 1, max 2 */}
9. WebXR — VR / AR on the web
Between setAnimationLoop and WebXRManager, WebXR in Three.js is nearly free. R3F has @react-three/xr.
import { XR, createXRStore, XROrigin } from '@react-three/xr'
const store = createXRStore()
<button onClick={() => store.enterVR()}>Enter VR</button>
<Canvas>
<XR store={store}>
<XROrigin />
{/* ...scene... */}
</XR>
</Canvas>
WebGPU + WebXR is surprisingly light in 2026 — Apple Vision Pro, Quest 3, Quest 3S all run WebGPU-backed WebXR cleanly. Adoption is picking up in marketing, education, and healthcare.
10. Three.js vs Babylon.js vs PlayCanvas
Engine comparison, short version.
| Item | Three.js | Babylon.js | PlayCanvas |
|---|---|---|---|
| License | MIT | Apache 2.0 | MIT (engine) |
| Strengths | Massive ecosystem, examples, community | Game features (physics, audio, material editor) | Visual editor, cloud IDE |
| Weaknesses | Game features you assemble yourself | Smaller ecosystem | Code-first is weak |
| WebGPU | Recommended in r182 | Stable since Babylon 7 | Engine-level support |
| Weekly npm DLs | ~2.7M | ~10K | ~8K |
| Sweet spot | Portfolios, products, art, viz | Browser games, simulation | Ads, games, configurators |
One-line picks:
- Creative work, art, portfolios, product viz → Three.js (+ R3F).
- Game-style interaction, heavy physics → Babylon.js.
- Editor-driven visual collaboration → PlayCanvas.
Ecosystem size dominates. When in doubt, Three.js.
11. Gaussian Splatting — photorealistic 3D without polygons
This is the new page.
Gaussian Splatting (gsplat) is not a polygon mesh. It represents a scene as millions of tiny 3D Gaussians — ellipsoids with position, color (in spherical harmonics), opacity, and orientation. The camera "splats" them onto the screen to form the image.
Polygon mesh: Gaussian Splatting:
┌── vertex / face data ┌── millions of Gaussians
├── UV, textures │ (position, SH color, scale, rotation, alpha)
├── normals, materials ├── no textures
└── shaded by lights └── lighting baked at capture time
Why is this exciting?
- Photorealistic — trained from 30 to several hundred photos / video. Output is near-photographic.
- Real-time — GPU-friendly. 60fps on the web is achievable.
- Zero mesh modeling — no Blender, no UVs, no textures, no normals. You need a camera, nothing else.
- The successor to NeRF — if NeRF was the academic milestone, gsplat is the practical tool (fast training, real-time render).
And clear limits
- Lighting is baked in — dynamically relighting the scene is hard.
- Collisions / physics are tough — no mesh to collide with.
- Editing is fiddly — purpose-built editors like SuperSplat exist for a reason.
- Files are big — tens to hundreds of MB.
The use case: capturing an existing space and shipping it. Real estate, heritage, museums, concerts, events, exhibitions.
The 2026 tooling landscape
- Polycam — the mobile capture market leader. iOS LiDAR + photogrammetry + gsplat. 4.7 average rating, 540k+ iOS reviews. Easiest on-ramp.
- Luma AI — cloud pipeline widely considered to produce the best visual quality among free consumer gsplat tools. Embeddable.
- SuperSplat — a free, open-source, browser-based gsplat editor built on PlayCanvas. Live annotations, hotspots, post-effects (bloom, vignette), camera animations, WebXR support. Export as an HTML viewer and host on GitHub Pages, Netlify, Vercel.
- NeRF Studio — research-oriented. Strong for local training and experimentation.
Putting it on the web — @mkkellogg/gaussian-splats-3d
A lightweight Three.js-compatible gsplat viewer. Inside R3F:
import { GaussianSplats3D } from '@mkkellogg/gaussian-splats-3d'
import { useThree } from '@react-three/fiber'
import { useEffect } from 'react'
function Splat({ url }) {
const { scene, camera, gl } = useThree()
useEffect(() => {
const viewer = new GaussianSplats3D.Viewer({
threeScene: scene,
camera,
renderer: gl,
selfDrivenMode: false,
})
viewer.addSplatScene(url)
return () => viewer.dispose()
}, [url, scene, camera, gl])
return null
}
Millions of Gaussians in your browser, 60fps. Five years ago this was science fiction.
12. AI-generated 3D — Meshy, Tripo, Rodin
The last branch. AI that produces 3D meshes from text or images. In 2026, no longer experimental — it is part of the workflow.
Three contenders.
- Meshy 6 — the most balanced product. Text-to-3D, image-to-3D, PBR textures, topology control, wide export support. 40–60s generation. The default recommendation.
- Tripo AI — fastest (20–30s). Sensible defaults, the lowest-friction entry. Text and image.
- Rodin AI (Gen-2) — 10-billion-parameter model. Highest quality. Strong on characters and structured assets. 60–180s.
A typical workflow:
- Concept — quick variations in Tripo (~30s).
- Favorite candidate — re-run through Meshy for clean PBR.
- Final character — Rodin Gen-2 for the high-quality pass.
- Export glTF — drop into Three.js / R3F.
Idea ─▶ Tripo (explore)
└─▶ Meshy (refine, PBR)
└─▶ Rodin (finish, characters)
└─▶ glTF
└─▶ useGLTF in R3F
Reality check: AI-generated topology is messy. Portfolios, viz, game backgrounds are fine, but characters that need rigging and animation typically require a retopo pass in Blender.
13. "What for what" — the use-case matrix
| Use case | Stack | Notes |
|---|---|---|
| Developer portfolio | R3F + drei + Bloom | A pinch of drei's Float / Text usually suffices |
| Product configurator | R3F + glTF + KTX2 | Color / texture options via material swap |
| Real-estate virtual tour | gsplat (Luma / Polycam) + SuperSplat | Photorealistic, real-space |
| Museum / exhibition | gsplat + WebXR | Hotspots, annotations |
| Browser game | Babylon.js or Three.js + Rapier | Physics, collision |
| Data visualization | R3F + camera choreography | Lean on instancing |
| AR marketing | R3F + @react-three/xr + WebXR | Pair with iOS Quick Look |
| Interactive art | Three.js + TSL (hand-rolled shaders) | Node shaders for freedom |
| Character-centric interaction | R3F + AI gen (Rodin) + Mixamo retarget | Plus a topology cleanup pass |
| LiDAR-captured assets | Polycam → glTF or gsplat | Phone-only end-to-end |
Rules of thumb:
- "Show a space as it is" → gsplat.
- "Let users manipulate it (color, options, physics)" → polygon (glTF) + R3F.
- "Build something small, fast" → R3F + AI gen.
14. Building a portfolio site — 30-minute recipe
The most common first project. Here is the skeleton, in one pass.
import { Canvas } from '@react-three/fiber'
import { OrbitControls, Environment, Float, Text3D, useGLTF, ContactShadows } from '@react-three/drei'
import { EffectComposer, Bloom } from '@react-three/postprocessing'
import { Suspense } from 'react'
function Hero() {
const { scene } = useGLTF('/hero.glb')
return <primitive object={scene} scale={1.4} />
}
export default function Portfolio() {
return (
<Canvas camera={{ position: [0, 0, 6], fov: 50 }} dpr={[1, 2]}>
<color attach="background" args={['#0a0a0a']} />
<Suspense fallback={null}>
<Environment preset="studio" />
<Float speed={1.5} rotationIntensity={0.4} floatIntensity={0.8}>
<Hero />
</Float>
<ContactShadows position={[0, -1.6, 0]} opacity={0.6} blur={2.4} />
</Suspense>
<OrbitControls enableZoom={false} />
<EffectComposer>
<Bloom intensity={0.8} mipmapBlur />
</EffectComposer>
</Canvas>
)
}
Checklist:
- Model from Meshy / Tripo, or a CC0 Sketchfab grab, then
gltf-transform optimizeonce. - Solid background plus a single Environment HDR (studio / city).
- Float, ContactShadows, Bloom to mask the "AI-gen look."
dpr={[1, 2]}to keep retina sane.- Kill Bloom on mobile (media query + conditional render).
Epilogue — the web really is 3D now
By 2026, web 3D is not a "look-once demo" slot any more. Product pages, real estate, museums, advertising, learning tools all carry 3D as a matter of course. The daily tooling:
- Three.js + R3F — polygon-based standard stack.
- WebGPU + TSL — two shader copies become one.
- Gaussian Splatting — a camera is enough for photorealistic space.
- AI 3D generation — a sentence is enough for a mesh.
Two parting artifacts.
A 14-item checklist
- Is WebGPURenderer your default, with WebGL 2 fallback verified?
- Did you remember the async
renderer.init()? - Are your glTF assets Draco / KTX2 compressed?
- Are dense identical meshes drawn through instancing, not one by one?
- Is
pixelRatiocapped on mobile? - Is frustum culling still on (you didn't disable it)?
- Are duplicate materials / geometries de-duplicated?
- Did you trim the number of post-processing passes on mobile?
- Is loading wrapped in Suspense for UX?
- Is WebXR entry inside a user gesture (click)?
- Are gsplat assets exported in a compressed format (SPZ / KSPLAT)?
- Did you retopologize AI-gen meshes (when needed)?
- Does
setAnimationLooprun in exactly one place (no duplicate loops)? - If the first frame is black — is it missing lights or missing init?
Ten anti-patterns
- Skipping
await renderer.init()on WebGPU and shipping a black first frame. - Using
MeshStandardMaterialwithout lights and rendering black. - Reloading the same model every frame.
- Disabling
frustumCulled"to be safe." - Uploading raw JPG/PNG instead of compressed textures.
- Identical
dprsettings on desktop and mobile. - Five post-processing passes on phones.
- Rigging AI-gen meshes without a retopo pass.
- Trying to edit / physics-collide gsplat like polygons.
- Calling
GLTFLoader.loadin each component instead ofuseGLTF.
Coming up next
Candidate posts: WebGPU compute shaders in practice — GPGPU for a million particles, The Gaussian Splatting workflow — from capture to web embed, R3F + Rapier — an interactive 3D game in an hour.
"The web really is 3D now. Polygons become meshes, photos become Gaussians, sentences become assets. The thread connecting them is still Three.js."
— 3D Development for the Web 2026, end.
References
- Three.js — official site
- Three.js — WebGPURenderer manual
- Three.js GitHub release notes
- React Three Fiber docs
- drei — helper library
- @react-three/postprocessing
- @react-three/xr
- WebGPU W3C specification
- WGSL specification
- TSL — a field guide
- Babylon.js — official
- PlayCanvas — official
- SuperSplat — gsplat editor
- Polycam — mobile 3D capture
- Luma AI
- NeRF Studio
- Meshy AI
- Tripo AI
- Rodin AI (Hyper3D)
- gltf-transform — optimization CLI
- Mkkellogg gaussian-splats-3d