--- name: threejs-tresjs description: 3D HUD rendering with Three.js and TresJS for JARVIS AI Assistant model: sonnet risk_level: MEDIUM version: 1.0.0 --- # Three.js / TresJS Development Skill > **File Organization**: This skill uses split structure. See `references/` for advanced patterns and security examples. ## 1. Overview This skill provides expertise for building 3D HUD interfaces using Three.js and TresJS (Vue 3 integration). It focuses on creating performant, visually stunning holographic displays for the JARVIS AI Assistant. **Risk Level**: MEDIUM - GPU resource consumption, potential ReDoS in color parsing, WebGL security considerations **Primary Use Cases**: - Rendering 3D holographic HUD panels - Animated status indicators and gauges - Particle effects for system visualization - Real-time metric displays with 3D elements ## 2. Core Responsibilities ### 2.1 Fundamental Principles 1. **TDD First**: Write tests before implementation - verify 3D components render correctly 2. **Performance Aware**: Optimize for 60fps with instancing, LOD, and efficient render loops 3. **Resource Management**: Always dispose of geometries, materials, and textures to prevent memory leaks 4. **Vue Reactivity Integration**: Use TresJS for seamless Vue 3 composition API integration 5. **Safe Color Parsing**: Validate color inputs to prevent ReDoS attacks 6. **GPU Protection**: Implement safeguards against GPU resource exhaustion 7. **Accessibility**: Provide fallbacks for devices without WebGL support ## 3. Technology Stack & Versions ### 3.1 Recommended Versions | Package | Version | Security Notes | |---------|---------|----------------| | three | ^0.160.0+ | Latest stable, fixes CVE-2020-28496 ReDoS | | @tresjs/core | ^4.0.0 | Vue 3 integration | | @tresjs/cientos | ^3.0.0 | Component library | | postprocessing | ^6.0.0 | Effects library | ### 3.2 Security-Critical Updates ```json { "dependencies": { "three": "^0.160.0", "@tresjs/core": "^4.0.0", "@tresjs/cientos": "^3.0.0" } } ``` **Note**: Versions before 0.137.0 have XSS vulnerabilities, before 0.125.0 have ReDoS vulnerabilities. ## 4. Implementation Patterns ### 4.1 Basic HUD Scene Setup ```vue ``` ### 4.2 Secure Color Handling ```typescript // utils/safeColor.ts import { Color } from 'three' // ✅ Safe color parsing with validation export function safeParseColor(input: string): Color { // Validate format to prevent ReDoS const hexPattern = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/ const rgbPattern = /^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/ if (!hexPattern.test(input) && !rgbPattern.test(input)) { console.warn('Invalid color format, using default') return new Color(0x00ff00) // Default JARVIS green } return new Color(input) } // ❌ DANGEROUS - User input directly to Color // const color = new Color(userInput) // Potential ReDoS // ✅ SECURE - Validated input const color = safeParseColor(userInput) ``` ### 4.3 Memory-Safe Component ```vue ``` ### 4.4 Performance-Optimized Instancing ```vue ``` ### 4.5 HUD Panel with Text ```vue ``` ## 5. Implementation Workflow (TDD) ### 5.1 TDD Process for 3D Components **Step 1: Write Failing Test First** ```typescript // tests/components/hud-panel.test.ts import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { mount, VueWrapper } from '@vue/test-utils' import { Scene, WebGLRenderer } from 'three' import HUDPanel from '~/components/hud/HUDPanel.vue' describe('HUDPanel', () => { let wrapper: VueWrapper beforeEach(() => { // Mock WebGL context for testing const canvas = document.createElement('canvas') const gl = canvas.getContext('webgl2') vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(gl) }) afterEach(() => { wrapper?.unmount() vi.restoreAllMocks() }) it('renders panel with correct dimensions', () => { wrapper = mount(HUDPanel, { props: { width: 2, height: 1, title: 'Status' } }) // Test fails until component is implemented expect(wrapper.exists()).toBe(true) }) it('disposes resources on unmount', async () => { wrapper = mount(HUDPanel, { props: { width: 2, height: 1, title: 'Status' } }) const disposeSpy = vi.fn() wrapper.vm.meshRef.geometry.dispose = disposeSpy wrapper.unmount() expect(disposeSpy).toHaveBeenCalled() }) }) ``` **Step 2: Implement Minimum to Pass** ```vue ``` **Step 3: Refactor Following Patterns** ```typescript // After tests pass, add performance optimizations // - Use instancing for multiple panels // - Add LOD for distant panels // - Implement texture atlases for text ``` **Step 4: Run Full Verification** ```bash # Run all tests npm test # Run with coverage npm test -- --coverage # Type check npm run typecheck # Performance benchmark npm run test:perf ``` ### 5.2 Testing 3D Animations ```typescript import { describe, it, expect, vi } from 'vitest' import { useRenderLoop } from '@tresjs/core' describe('Animation Loop', () => { it('maintains 60fps during animation', async () => { const frameTimes: number[] = [] let lastTime = performance.now() const { onLoop } = useRenderLoop() onLoop(() => { const now = performance.now() frameTimes.push(now - lastTime) lastTime = now }) // Simulate 60 frames await new Promise(resolve => setTimeout(resolve, 1000)) const avgFrameTime = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length expect(avgFrameTime).toBeLessThan(16.67) // 60fps = 16.67ms per frame }) it('cleans up animation loop on unmount', () => { const cleanup = vi.fn() const { pause } = useRenderLoop() // Component unmounts pause() expect(cleanup).not.toThrow() }) }) ``` ### 5.3 Testing Resource Disposal ```typescript describe('Resource Management', () => { it('disposes all GPU resources', () => { const geometry = new BoxGeometry(1, 1, 1) const material = new MeshStandardMaterial({ color: 0x00ff41 }) const mesh = new Mesh(geometry, material) const geoDispose = vi.spyOn(geometry, 'dispose') const matDispose = vi.spyOn(material, 'dispose') // Cleanup function mesh.geometry.dispose() mesh.material.dispose() expect(geoDispose).toHaveBeenCalled() expect(matDispose).toHaveBeenCalled() }) it('handles material arrays correctly', () => { const materials = [ new MeshBasicMaterial(), new MeshStandardMaterial() ] const mesh = new Mesh(new BoxGeometry(), materials) const spies = materials.map(m => vi.spyOn(m, 'dispose')) materials.forEach(m => m.dispose()) spies.forEach(spy => expect(spy).toHaveBeenCalled()) }) }) ``` ## 6. Performance Patterns ### 6.1 Geometry Instancing ```typescript // Good: Use InstancedMesh for repeated objects import { InstancedMesh, Matrix4, Object3D } from 'three' const COUNT = 1000 const mesh = new InstancedMesh(geometry, material, COUNT) const dummy = new Object3D() for (let i = 0; i < COUNT; i++) { dummy.position.set(Math.random() * 10, Math.random() * 10, Math.random() * 10) dummy.updateMatrix() mesh.setMatrixAt(i, dummy.matrix) } mesh.instanceMatrix.needsUpdate = true // Bad: Creating individual meshes for (let i = 0; i < COUNT; i++) { const mesh = new Mesh(geometry.clone(), material.clone()) // Memory waste! scene.add(mesh) } ``` ### 6.2 Texture Atlases ```typescript // Good: Single texture atlas for multiple sprites const atlas = new TextureLoader().load('/textures/hud-atlas.png') const materials = { panel: new SpriteMaterial({ map: atlas }), icon: new SpriteMaterial({ map: atlas }) } // Set UV offsets for different sprites materials.panel.map.offset.set(0, 0.5) materials.panel.map.repeat.set(0.5, 0.5) // Bad: Loading separate textures const panelTex = new TextureLoader().load('/textures/panel.png') const iconTex = new TextureLoader().load('/textures/icon.png') // Multiple draw calls, more GPU memory ``` ### 6.3 Level of Detail (LOD) ```typescript // Good: Use LOD for complex objects import { LOD } from 'three' const lod = new LOD() // High detail - close up const highDetail = new Mesh( new SphereGeometry(1, 32, 32), material ) lod.addLevel(highDetail, 0) // Medium detail - mid range const medDetail = new Mesh( new SphereGeometry(1, 16, 16), material ) lod.addLevel(medDetail, 10) // Low detail - far away const lowDetail = new Mesh( new SphereGeometry(1, 8, 8), material ) lod.addLevel(lowDetail, 20) scene.add(lod) // Bad: Always rendering high detail const sphere = new Mesh(new SphereGeometry(1, 64, 64), material) ``` ### 6.4 Frustum Culling ```typescript // Good: Enable frustum culling (default, but verify) mesh.frustumCulled = true // For custom bounds optimization mesh.geometry.computeBoundingSphere() mesh.geometry.computeBoundingBox() // Manual visibility check for complex scenes const frustum = new Frustum() const matrix = new Matrix4().multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ) frustum.setFromProjectionMatrix(matrix) objects.forEach(obj => { obj.visible = frustum.intersectsObject(obj) }) // Bad: Disabling culling or rendering everything mesh.frustumCulled = false // Renders even when off-screen ``` ### 6.5 Object Pooling ```typescript // Good: Pool and reuse objects class ParticlePool { private pool: Mesh[] = [] private active: Set = new Set() constructor(private geometry: BufferGeometry, private material: Material) { // Pre-allocate pool for (let i = 0; i < 100; i++) { const mesh = new Mesh(geometry, material) mesh.visible = false this.pool.push(mesh) } } acquire(): Mesh | null { const mesh = this.pool.find(m => !this.active.has(m)) if (mesh) { mesh.visible = true this.active.add(mesh) return mesh } return null } release(mesh: Mesh): void { mesh.visible = false this.active.delete(mesh) } } // Bad: Creating/destroying objects each frame function spawnParticle() { const mesh = new Mesh(geometry, material) // GC pressure! scene.add(mesh) setTimeout(() => { scene.remove(mesh) mesh.geometry.dispose() }, 1000) } ``` ### 6.6 RAF Optimization ```typescript // Good: Efficient render loop let lastTime = 0 const targetFPS = 60 const frameInterval = 1000 / targetFPS function animate(currentTime: number) { requestAnimationFrame(animate) const delta = currentTime - lastTime // Skip frame if too soon (for battery saving) if (delta < frameInterval) return lastTime = currentTime - (delta % frameInterval) // Update only what changed if (needsUpdate) { updateScene() renderer.render(scene, camera) } } // Bad: Rendering every frame unconditionally function animate() { requestAnimationFrame(animate) // Always updates everything updateAllObjects() renderer.render(scene, camera) // Even if nothing changed } ``` ### 6.7 Shader Optimization ```typescript // Good: Simple, optimized shaders const material = new ShaderMaterial({ vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` varying vec2 vUv; uniform vec3 color; void main() { gl_FragColor = vec4(color, 1.0); } `, uniforms: { color: { value: new Color(0x00ff41) } } }) // Bad: Complex calculations in fragment shader // Avoid: loops, conditionals, texture lookups when possible ``` ## 7. Security Standards ### 7.1 Known Vulnerabilities | CVE | Severity | Description | Mitigation | |-----|----------|-------------|------------| | CVE-2020-28496 | HIGH | ReDoS in color parsing | Update to 0.125.0+, validate colors | | CVE-2022-0177 | MEDIUM | XSS in docs | Update to 0.137.0+ | ### 7.2 OWASP Top 10 Coverage | OWASP Category | Risk | Mitigation | |----------------|------|------------| | A05 Injection | MEDIUM | Validate all color/text inputs | | A06 Vulnerable Components | HIGH | Keep Three.js updated | ### 7.3 GPU Resource Protection ```typescript // composables/useResourceLimit.ts export function useResourceLimit() { const MAX_TRIANGLES = 1_000_000 const MAX_DRAW_CALLS = 100 let triangleCount = 0 function checkGeometry(geometry: BufferGeometry): boolean { const triangles = geometry.index ? geometry.index.count / 3 : geometry.attributes.position.count / 3 if (triangleCount + triangles > MAX_TRIANGLES) { console.error('Triangle limit exceeded') return false } triangleCount += triangles return true } return { checkGeometry } } ``` ## 6. Testing & Quality ### 6.1 Component Testing ```typescript import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' describe('HUD Panel', () => { it('sanitizes malicious title input', () => { const wrapper = mount(HUDPanel, { props: { title: 'Status', value: 75 } }) expect(wrapper.vm.safeTitle).not.toContain('