webgl について
JARVIS AI Assistant HUD 向けに、カスタムシェーダーや視覚効果の実装を扱う WebGL 開発スキルです。GPU 性能、リソース保護、コンテキスト喪失への対応、テスト手順を説明します。
主な機能
- 01カスタムシェーダーと視覚効果の作成
- 02GPU リソースとメモリ上限の管理
- 03シェーダー入力の検証
- 04WebGL コンテキスト喪失への対応
- 05性能改善とテストのパターン
活用シーン
- 01ホログラフィックなシェーダー効果の作成
- 02bloom や glitch のポストプロセス追加
- 03パーティクルシステムとリアルタイム可視化の構築
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ martinholovsky/ claude-skills-generator --skill webglインストール後、Claude Code で/webglを入力して読み込まれたか確認します。
同梱ファイルreferences/advanced-patterns.mdreferences/security-examples.md
詳細
WebGL 開発スキル
ファイル構成:このスキルは分割構成を採用しています。高度なパターンとセキュリティの例については、
references/を参照してください。
1. 概要
このスキルでは、JARVIS AI Assistant HUD でカスタムシェーダーや視覚効果を作成するための WebGL の専門知識を提供します。セキュリティ上の考慮事項を踏まえた、GPU アクセラレーションによるレンダリングに重点を置いています。
リスクレベル:中 — GPU への直接アクセス、リソース枯渇の可能性、ドライバーの脆弱性
主なユースケース:
- ホログラフィック効果用のカスタムシェーダー
- ポストプロセス効果(ブルーム、グリッチ)
- コンピュートシェーダーを使用したパーティクルシステム
- リアルタイムのデータ可視化
2. 主な責務
2.1 基本原則
- TDD を優先:実装前にテストを作成します。シェーダー、コンテキスト、リソースをテストします
- パフォーマンスを考慮:GPU の使用を最適化します。描画をバッチ処理し、バッファーを再利用し、テクスチャーを圧縮します
- GPU の安全性:タイムアウトの仕組みとリソース制限を実装します
- シェーダーの検証:コンパイル前にすべてのシェーダー入力を検証します
- コンテキスト管理:コンテキスト喪失を適切に処理します
- パフォーマンス予算:描画呼び出し数と三角形数に厳格な上限を設定します
- フォールバック戦略:WebGL を使用しない場合のフォールバックを用意します
- メモリ管理:テクスチャーとバッファーの使用量を追跡し、制限します
3. 技術スタックとバージョン
3.1 ブラウザーのサポート状況
| ブラウザー | WebGL 2.0 | 備考 |
|---|---|---|
| Chrome | 56+ | 完全にサポート |
| Firefox | 51+ | 完全にサポート |
| Safari | 15+ | WebGL 2.0 をサポート |
| Edge | 79+ | Chromium ベース |
3.2 セキュリティ上の考慮事項
// Check WebGL support and capabilities
function getWebGLContext(canvas: HTMLCanvasElement): WebGL2RenderingContext | null {
const gl = canvas.getContext('webgl2', {
alpha: true,
antialias: true,
powerPreference: 'high-performance',
failIfMajorPerformanceCaveat: true // Fail if software rendering
})
if (!gl) {
console.warn('WebGL 2.0 not supported')
return null
}
return gl
}
4. 実装パターン
4.1 安全なシェーダーのコンパイル
// utils/shaderUtils.ts
// ✅ Safe shader compilation with error handling
export function compileShader(
gl: WebGL2RenderingContext,
source: string,
type: number
): WebGLShader | null {
const shader = gl.createShader(type)
if (!shader) return null
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const error = gl.getShaderInfoLog(shader)
console.error('Shader compilation error:', error)
gl.deleteShader(shader)
return null
}
return shader
}
// ✅ Safe program linking
export function createProgram(
gl: WebGL2RenderingContext,
vertexShader: WebGLShader,
fragmentShader: WebGLShader
): WebGLProgram | null {
const program = gl.createProgram()
if (!program) return null
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const error = gl.getProgramInfoLog(program)
console.error('Program linking error:', error)
gl.deleteProgram(program)
return null
}
return program
}
4.2 コンテキスト喪失の処理
// composables/useWebGL.ts
export function useWebGL(canvas: Ref<HTMLCanvasElement | null>) {
const gl = ref<WebGL2RenderingContext | null>(null)
const contextLost = ref(false)
onMounted(() => {
if (!canvas.value) return
// ✅ Handle context loss
canvas.value.addEventListener('webglcontextlost', (e) => {
e.preventDefault()
contextLost.value = true
console.warn('WebGL context lost')
})
canvas.value.addEventListener('webglcontextrestored', () => {
contextLost.value = false
initializeGL()
console.info('WebGL context restored')
})
initializeGL()
})
function initializeGL() {
gl.value = getWebGLContext(canvas.value!)
// Reinitialize all resources
}
return { gl, contextLost }
}
4.3 ホログラフィックシェーダー
// shaders/holographic.frag
#version 300 es
precision highp float;
uniform float uTime;
uniform vec3 uColor;
uniform float uScanlineIntensity;
in vec2 vUv;
out vec4 fragColor;
void main() {
// Scanline effect
float scanline = sin(vUv.y * 200.0 + uTime * 2.0) * 0.5 + 0.5;
scanline = mix(1.0, scanline, uScanlineIntensity);
// Edge glow
float edge = smoothstep(0.0, 0.1, vUv.x) *
smoothstep(1.0, 0.9, vUv.x) *
smoothstep(0.0, 0.1, vUv.y) *
smoothstep(1.0, 0.9, vUv.y);
vec3 color = uColor * scanline * edge;
float alpha = edge * 0.8;
fragColor = vec4(color, alpha);
}
4.4 リソース管理
// utils/resourceManager.ts
export class WebGLResourceManager {
private textures: Set<WebGLTexture> = new Set()
private buffers: Set<WebGLBuffer> = new Set()
private programs: Set<WebGLProgram> = new Set()
private textureMemory = 0
private readonly MAX_TEXTURE_MEMORY = 256 * 1024 * 1024 // 256MB
constructor(private gl: WebGL2RenderingContext) {}
createTexture(width: number, height: number): WebGLTexture | null {
const size = width * height * 4 // RGBA
// ✅ Enforce memory limits
if (this.textureMemory + size > this.MAX_TEXTURE_MEMORY) {
console.error('Texture memory limit exceeded')
return null
}
const texture = this.gl.createTexture()
if (texture) {
this.textures.add(texture)
this.textureMemory += size
}
return texture
}
dispose(): void {
this.textures.forEach(t => this.gl.deleteTexture(t))
this.buffers.forEach(b => this.gl.deleteBuffer(b))
this.programs.forEach(p => this.gl.deleteProgram(p))
this.textureMemory = 0
}
}
4.5 ユニフォームの検証
// ✅ Type-safe uniform setting
export function setUniforms(
gl: WebGL2RenderingContext,
program: WebGLProgram,
uniforms: Record<string, number | number[] | Float32Array>
): void {
for (const [name, value] of Object.entries(uniforms)) {
const location = gl.getUniformLocation(program, name)
if (!location) {
console.warn(`Uniform '${name}' not found`)
continue
}
if (typeof value === 'number') {
gl.uniform1f(location, value)
} else if (Array.isArray(value)) {
switch (value.length) {
case 2: gl.uniform2fv(location, value); break
case 3: gl.uniform3fv(location, value); break
case 4: gl.uniform4fv(location, value); break
case 16: gl.uniformMatrix4fv(location, false, value); break
}
}
}
}
5. 実装ワークフロー(TDD)
5.1 手順
- 失敗するテストを書く -> 2. 最小限の実装を行う -> 3. リファクタリングする -> 4. 検証する
// Step 1: tests/webgl/shaderCompilation.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { compileShader } from '@/utils/shaderUtils'
describe('WebGL Shader Compilation', () => {
let gl: WebGL2RenderingContext
beforeEach(() => {
gl = document.createElement('canvas').getContext('webgl2')!
})
it('should compile valid shader', () => {
const source = `#version 300 es
in vec4 aPosition;
void main() { gl_Position = aPosition; }`
expect(compileShader(gl, source, gl.VERTEX_SHADER)).not.toBeNull()
})
it('should return null for invalid shader', () => {
expect(compileShader(gl, 'invalid', gl.FRAGMENT_SHADER)).toBeNull()
})
})
// Step 2-3: Implement and refactor (see section 4.1)
// Step 4: npm test && npm run typecheck && npm run build
5.2 コンテキストとリソースのテスト
describe('WebGL Context', () => {
it('should handle context loss', async () => {
const { gl, contextLost } = useWebGL(ref(canvas))
gl.value?.getExtension('WEBGL_lose_context')?.loseContext()
await nextTick()
expect(contextLost.value).toBe(true)
})
})
describe('Resource Manager', () => {
it('should enforce memory limits', () => {
const manager = new WebGLResourceManager(gl)
expect(manager.createTexture(1024, 1024)).not.toBeNull()
expect(manager.createTexture(16384, 16384)).toBeNull() // Exceeds limit
})
})
6. パフォーマンスパターン
6.1 バッファーの再利用
// Bad - Creates new buffer every frame
const buffer = gl.createBuffer()
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW)
gl.deleteBuffer(buffer)
// Good - Reuse buffer, update only data
gl.bufferSubData(gl.ARRAY_BUFFER, 0, data) // Update existing buffer
6.2 描画呼び出しのバッチ処理
// Bad - One draw call per object
objects.forEach(obj => {
gl.useProgram(obj.program)
gl.drawElements(...)
})
// Good - Batch by material/shader
const batches = groupByMaterial(objects)
batches.forEach(batch => {
gl.useProgram(batch.program)
batch.objects.forEach(obj => gl.drawElements(...))
})
6.3 テクスチャーの圧縮
// Bad - Always uncompressed RGBA
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)
// Good - Use compressed formats when available
const ext = gl.getExtension('WEBGL_compressed_texture_s3tc')
if (ext) gl.compressedTexImage2D(gl.TEXTURE_2D, 0, ext.COMPRESSED_RGBA_S3TC_DXT5_EXT, ...)
6.4 インスタンス化レンダリング
// Bad - Individual draw calls for particles
particles.forEach(p => {
gl.uniform3fv(uPosition, p.position)
gl.drawArrays(gl.TRIANGLES, 0, 6)
})
// Good - Single instanced draw call
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, particles.length)
6.5 VAO の使用
// Bad - Rebind attributes every frame
gl.enableVertexAttribArray(0)
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0)
// Good - Use VAO to store attribute state
const vao = gl.createVertexArray()
gl.bindVertexArray(vao)
// Set up once, then just bind VAO for rendering
7. セキュリティ基準
7.1 既知の脆弱性
| CVE | 深刻度 | 説明 | 緩和策 |
|---|---|---|---|
| CVE-2024-11691 | 高 | Apple M シリーズのメモリ破損 | ブラウザーを更新し、OS のパッチを適用する |
| CVE-2023-1531 | 高 | Chrome の解放後使用 | Chrome を更新する |
7.2 OWASP Top 10 の対応状況
| OWASP カテゴリー | リスク | 緩和策 |
|---|---|---|
| A06 脆弱なコンポーネント | 高 | ブラウザーを最新の状態に保つ |
| A10 SSRF | 低 | ブラウザーによるコンテキスト分離 |
7.3 GPU リソースの保護
// ✅ Implement resource limits
const LIMITS = {
maxDrawCalls: 100,
maxTriangles: 1_000_000,
maxTextures: 32,
maxTextureSize: 4096
}
function checkLimits(stats: RenderStats): boolean {
if (stats.drawCalls > LIMITS.maxDrawCalls) {
console.error('Draw call limit exceeded')
return false
}
if (stats.triangles > LIMITS.maxTriangles) {
console.error('Triangle limit exceeded')
return false
}
return true
}
8. よくある間違いとアンチパターン
8.1 重大なセキュリティ上のアンチパターン
禁止:コンテキスト喪失の処理を省略する
// ❌ DANGEROUS - App crashes on context loss
const gl = canvas.getContext('webgl2')
// No context loss handler!
// ✅ SECURE - Handle gracefully
canvas.addEventListener('webglcontextlost', handleLoss)
canvas.addEventListener('webglcontextrestored', handleRestore)
禁止:リソース割り当てを無制限にする
// ❌ DANGEROUS - GPU memory exhaustion
for (let i = 0; i < userCount; i++) {
textures.push(gl.createTexture())
}
// ✅ SECURE - Enforce limits
if (textureCount < MAX_TEXTURES) {
textures.push(gl.createTexture())
}
8.2 パフォーマンス上のアンチパターン
回避:状態を過剰に変更する
// ❌ BAD - Unbatched draw calls
objects.forEach(obj => {
gl.useProgram(obj.program)
gl.bindTexture(gl.TEXTURE_2D, obj.texture)
gl.drawElements(...)
})
// ✅ GOOD - Batch by material
batches.forEach(batch => {
gl.useProgram(batch.program)
gl.bindTexture(gl.TEXTURE_2D, batch.texture)
batch.objects.forEach(obj => gl.drawElements(...))
})
9. 実装前チェックリスト
フェーズ 1:コードを書く前
- シェーダー、コンテキスト、リソースの失敗するテストを作成する
- パフォーマンス予算を定める(描画呼び出し数 <100、メモリ <256MB)
- 必要な WebGL 拡張機能を特定する
フェーズ 2:実装中
- 復旧処理を含むコンテキスト喪失への対応
- リソース制限とメモリ使用量の追跡
- コンパイル前のシェーダー検証
- VAO を使用し、描画をバッチ処理し、バッファーを再利用する
- パーティクルにインスタンス化レンダリングを使用する
フェーズ 3:コミット前
- テストに合格する:
npm test -- --run tests/webgl/ - 型チェック:
npm run typecheck - ビルド:
npm run build - パフォーマンスを検証する(描画呼び出し数、メモリ)
- WebGL を利用できない場合のフォールバックをテストする
10. まとめ
WebGL は JARVIS HUD に GPU アクセラレーションによるグラフィックスを提供します。主な原則は、コンテキスト喪失に対応し、リソース制限を適用し、シェーダーを検証し、メモリを追跡し、描画呼び出しをバッチ処理し、状態変更を最小限に抑えることです。
覚えておくこと:WebGL はブラウザーのサンドボックスを回避するため、リソースの枯渇から常に保護してください。
参照:references/advanced-patterns.md、references/security-examples.md
コメント
Markdown 対応