원클릭으로
threejs
Three.js 3D 視覺化最佳實踐指南(r183+)。當需要建立 3D 場景、設定 renderer/camera/lights、使用 OrbitControls、做 raycasting 互動、效能優化、或整合 Astro 時使用。
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Three.js 3D 視覺化最佳實踐指南(r183+)。當需要建立 3D 場景、設定 renderer/camera/lights、使用 OrbitControls、做 raycasting 互動、效能優化、或整合 Astro 時使用。
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Zod v4 schema validation 最佳實踐指南。當需要定義 schema、驗證/解析 JSON 資料、type inference、或處理 unknown data 時使用。
Svelte 5 + Astro 整合最佳實踐指南。當需要建立 Svelte 元件、使用 runes API、整合 Astro islands、或用 Testing Library 測試 Svelte 元件時使用。
GitHub GraphQL API 最佳實踐指南。當需要使用 GraphQL 查詢使用者資料、處理 cursor pagination、計算 rate limit、或除錯 GraphQL errors 時使用。
gayanvoice/top-github-users 架構參考指南。當需要了解 GitHub 使用者排行榜的資料抓取管線、國家設定、排行計算邏輯、已知問題、或社群需求時使用。
Commander.js v14 CLI 框架最佳實踐。當需要建立 CLI 工具、解析命令列參數、設計 subcommands 時使用。
GitHub Actions CI/CD 最佳實踐指南。當需要設定 workflow、cron 排程、GitHub Pages 部署、使用 Octokit API、或處理 rate limiting 時使用。
| name | threejs |
| description | Three.js 3D 視覺化最佳實踐指南(r183+)。當需要建立 3D 場景、設定 renderer/camera/lights、使用 OrbitControls、做 raycasting 互動、效能優化、或整合 Astro 時使用。 |
three/webgpu 匯入自動 fallback WebGL2// 核心(tree-shakeable)
import { Scene, PerspectiveCamera, WebGLRenderer } from 'three';
// WebGPU(含 WebGL2 自動 fallback)
import * as THREE from 'three/webgpu';
// Addons(controls、loaders 等)
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { Timer } from 'three/addons/misc/Timer.js';
注意:盡量 import 個別 class 而非 import * as THREE,減少 bundle size。
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75, // FOV
container.clientWidth / container.clientHeight, // aspect
0.1, // near
1000 // far
);
camera.position.set(0, 1.5, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 上限 2
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
container.appendChild(renderer.domElement);
import { Timer } from 'three/addons/misc/Timer.js';
const timer = new Timer();
timer.connect(document); // 自動處理 Page Visibility
function animate(timestamp: number) {
requestAnimationFrame(animate);
timer.update(timestamp);
const delta = timer.getDelta();
mesh.rotation.y += 1.0 * delta; // frame-rate independent
controls.update(); // enableDamping 時必須
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
為何用 Timer 而非 Clock:
getDelta() 同一 frame 多次呼叫回傳一致值(Clock 不會)setTimescale() 做慢動作/快轉const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 2;
controls.maxDistance = 20;
controls.maxPolarAngle = Math.PI / 2; // 防止穿過地面
controls.target.set(0, 1, 0);
controls.update();
清除:controls.dispose() 會移除所有 event listeners。
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
let hoveredObject: THREE.Object3D | null = null;
function onPointerMove(event: PointerEvent) {
const rect = renderer.domElement.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
}
function checkHover() {
raycaster.setFromCamera(pointer, camera);
// 只對互動物件做 raycast,不要整個 scene
const intersects = raycaster.intersectObjects(interactiveObjects);
if (intersects.length > 0) {
const obj = intersects[0].object;
if (hoveredObject !== obj) {
// reset previous
if (hoveredObject) (hoveredObject as THREE.Mesh).material.emissive?.setHex(0x000000);
hoveredObject = obj;
(obj as THREE.Mesh).material.emissive?.setHex(0x333333);
renderer.domElement.style.cursor = 'pointer';
}
} else if (hoveredObject) {
(hoveredObject as THREE.Mesh).material.emissive?.setHex(0x000000);
hoveredObject = null;
renderer.domElement.style.cursor = 'default';
}
}
renderer.domElement.addEventListener('pointermove', onPointerMove);
注意:搭配 OrbitControls 時,區分 click vs drag — 比較 pointerdown/pointerup 的距離(閾值 ~3px)。
const resizeObserver = new ResizeObserver(entries => {
const { width, height } = entries[0].contentRect;
camera.aspect = width / height;
camera.updateProjectionMatrix(); // 必須!
renderer.setSize(width, height, false);
});
resizeObserver.observe(container);
島嶼式元件用 ResizeObserver(非 window.resize)。
function dispose(object: THREE.Object3D) {
object.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh;
mesh.geometry.dispose();
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
materials.forEach(mat => {
Object.values(mat).forEach(val => {
if (val && typeof val === 'object' && 'dispose' in val) (val as any).dispose();
});
mat.dispose();
});
}
});
}
// 也要 dispose:
renderer.dispose();
controls.dispose();
規則:JS GC 不會釋放 GPU 資源,必須手動 .dispose()。
| 策略 | 說明 |
|---|---|
| Draw calls < 100 | 用 InstancedMesh(重複 geometry)或 BatchedMesh(不同 geometry) |
| Pixel ratio 上限 | Desktop: 2, Mobile: 1.5 |
| 關閉不需要的功能 | { stencil: false, depth: false, alpha: false } |
| Tab 隱藏暫停渲染 | Timer 自動處理,或用 Page Visibility API |
| 使用 LOD | THREE.LOD 依距離切換細節 |
| 壓縮模型 | Draco 壓縮 geometry(減 90-95%)、KTX2 壓縮 texture(減 ~10x 記憶體) |
| 偏好 env map | 環境貼圖比動態光源效能好 |
檢查效能:renderer.info.render.calls、stats-gl
import WebGL from 'three/addons/capabilities/WebGL.js';
if (WebGL.isWebGL2Available()) {
initScene();
} else {
const warning = WebGL.getWebGL2ErrorMessage();
container.appendChild(warning);
}
WebGPURenderer 自帶 WebGL2 fallback,不需要額外偵測。
---
// src/components/ThreeScene.astro
---
<div id="three-canvas" class="w-full h-screen"></div>
<script>
import { initScene } from '../lib/three/scene';
const container = document.getElementById('three-canvas')!;
const cleanup = initScene(container);
// Astro View Transitions cleanup
document.addEventListener('astro:before-swap', cleanup);
</script>
// src/lib/three/scene.ts
export function initScene(container: HTMLElement): () => void {
// ... setup scene, camera, renderer, controls ...
let animationId: number;
function animate() {
animationId = requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
return () => {
cancelAnimationFrame(animationId);
renderer.dispose();
controls.dispose();
// dispose all geometries, materials, textures
};
}
要點:
<script> tag(Astro 處理為 module)astro:before-swap 做 cleanupResizeObserver 而非 window.resizepointer events(統一 mouse + touch)touch-action: none 防止瀏覽器手勢getDelta() 返回 ~0updateProjectionMatrix()touch-action: none → 手機滑動卡頓