mcpField
W
webgl

webgl

github.com/martinholovskyデザイン・UI48556 回インストール47 週間前に更新
リポジトリ

webgl について

JARVIS AI Assistant HUD 向けに、カスタムシェーダーや視覚効果の実装を扱う WebGL 開発スキルです。GPU 性能、リソース保護、コンテキスト喪失への対応、テスト手順を説明します。

主な機能

  1. 01カスタムシェーダーと視覚効果の作成
  2. 02GPU リソースとメモリ上限の管理
  3. 03シェーダー入力の検証
  4. 04WebGL コンテキスト喪失への対応
  5. 05性能改善とテストのパターン

活用シーン

  1. 01ホログラフィックなシェーダー効果の作成
  2. 02bloom や glitch のポストプロセス追加
  3. 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 基本原則
  1. TDD を優先:実装前にテストを作成します。シェーダー、コンテキスト、リソースをテストします
  2. パフォーマンスを考慮:GPU の使用を最適化します。描画をバッチ処理し、バッファーを再利用し、テクスチャーを圧縮します
  3. GPU の安全性:タイムアウトの仕組みとリソース制限を実装します
  4. シェーダーの検証:コンパイル前にすべてのシェーダー入力を検証します
  5. コンテキスト管理:コンテキスト喪失を適切に処理します
  6. パフォーマンス予算:描画呼び出し数と三角形数に厳格な上限を設定します
  7. フォールバック戦略:WebGL を使用しない場合のフォールバックを用意します
  8. メモリ管理:テクスチャーとバッファーの使用量を追跡し、制限します

3. 技術スタックとバージョン

3.1 ブラウザーのサポート状況
ブラウザーWebGL 2.0備考
Chrome56+完全にサポート
Firefox51+完全にサポート
Safari15+WebGL 2.0 をサポート
Edge79+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 手順
  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

コメント