← レッスン一覧に戻る レッスン 33 / 48
上級 graphics
WebGLによる3Dグラフィックス
WebGLレンダリングパイプライン
WebGLはOpenGL ES 2.0にほぼ1:1でマッピングされる低レベルグラフィックスAPIです。Rust/Wasmからレンダリングパイプラインの各段階を制御できます:
┌────────────────────────────────────────────────────────────────┐
│ WebGLレンダリングパイプライン │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ 頂点 │──►│ 頂点 │──►│ プリミティブ組立 │ │
│ │ バッファ │ │ シェーダ │ │ (三角形/線分) │ │
│ │(データ) │ │(頂点ごと) │ │ │ │
│ └──────────┘ └──────────────┘ └───────────┬────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ フレーム │◄──│ フラグメント │◄──│ ラスタライゼーション │ │
│ │ バッファ │ │ シェーダ │ │(三角形→ピクセル) │ │
│ │(画面) │ │(ピクセルごと)│ │ │ │
│ └──────────┘ └──────────────┘ └────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘RustからのWebGLセットアップ
[dependencies.web-sys]
version = "0.3"
features = [
"HtmlCanvasElement",
"WebGlRenderingContext",
"WebGlProgram",
"WebGlShader",
"WebGlBuffer",
"WebGlUniformLocation",
]use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{WebGlRenderingContext as GL, WebGlProgram, WebGlShader};
#[wasm_bindgen]
pub fn init_webgl(canvas_id: &str) -> Result<(), JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id(canvas_id).unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()?;
let gl: GL = canvas.get_context("webgl")?
.unwrap().dyn_into()?;
gl.viewport(0, 0, canvas.width() as i32, canvas.height() as i32);
gl.clear_color(0.0, 0.0, 0.0, 1.0);
gl.enable(GL::DEPTH_TEST);
gl.clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT);
Ok(())
}シェーダ
シェーダはGPU上で実行される小さなプログラムです。WebGLには2種類必要です:
頂点シェーダ
頂点ごとに1回実行されます。3D座標をスクリーン座標に変換します:
attribute vec3 aPosition;
attribute vec3 aNormal;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
varying vec3 vNormal;
varying vec3 vFragPos;
void main() {
vec4 worldPos = uModel * vec4(aPosition, 1.0);
vFragPos = worldPos.xyz;
vNormal = mat3(uModel) * aNormal; // 法線の変換
gl_Position = uProjection * uView * worldPos;
}フラグメントシェーダ
ピクセルごとに1回実行されます。最終的な色を決定します:
precision mediump float;
varying vec3 vNormal;
varying vec3 vFragPos;
uniform vec3 uLightDir;
uniform vec3 uLightColor;
uniform vec3 uObjectColor;
void main() {
// 環境光
float ambient = 0.1;
// 拡散光
vec3 norm = normalize(vNormal);
float diff = max(dot(norm, uLightDir), 0.0);
vec3 result = (ambient + diff) * uLightColor * uObjectColor;
gl_FragColor = vec4(result, 1.0);
}Rustからのシェーダコンパイル
fn compile_shader(gl: &GL, shader_type: u32, source: &str) -> Result<WebGlShader, String> {
let shader = gl.create_shader(shader_type)
.ok_or("シェーダを作成できません")?;
gl.shader_source(&shader, source);
gl.compile_shader(&shader);
if gl.get_shader_parameter(&shader, GL::COMPILE_STATUS).as_bool().unwrap_or(false) {
Ok(shader)
} else {
Err(gl.get_shader_info_log(&shader).unwrap_or_default())
}
}
fn link_program(gl: &GL, vert: &WebGlShader, frag: &WebGlShader) -> Result<WebGlProgram, String> {
let program = gl.create_program().ok_or("プログラムを作成できません")?;
gl.attach_shader(&program, vert);
gl.attach_shader(&program, frag);
gl.link_program(&program);
if gl.get_program_parameter(&program, GL::LINK_STATUS).as_bool().unwrap_or(false) {
Ok(program)
} else {
Err(gl.get_program_info_log(&program).unwrap_or_default())
}
}行列演算:3Dの基礎
3Dシーンのすべてのオブジェクトは3つの行列変換を経ます:
モデル ビュー 射影
ローカル空間 ──────────► ワールド ─────────► カメラ ──────────► スクリーン
(オブジェクト) 空間 空間 (NDC)
MVP = Projection * View * Model| 行列 | 目的 | 例 |
|---|---|---|
| Model | オブジェクトの位置/回転/スケーリング | キューブを45度回転 |
| View | カメラの配置 | カメラを(0, 2, 5)に置いて原点を見る |
| Projection | 3D → 2D(透視変換) | 視野角60度 |
回転行列
各軸周りの回転にはsinとcosを使用します:
Y軸周りにθ回転:
┌ cos θ 0 sin θ 0 ┐
│ 0 1 0 0 │
│ -sin θ 0 cos θ 0 │
└ 0 0 0 1 ┘透視投影
透視投影行列は奥行きの錯覚を生み出します — 遠くのオブジェクトほど小さく見えます:
視錐台(視野体積):
近平面
┌───────┐
/│ │\
/ │ │ \
/ │ │ \ 遠平面
/ │ │ \ ┌─────────────┐
/ FOV│ │ \ │ │
/ │ 視点 │ \ │ │
/ │ * │ \ │ │
\ │ │ / │ │
\ │ │ / │ │
\ │ │ / │ │
\ └───────┘ / └─────────────┘
\ /
near=0.1 far=100.0キューブジオメトリ
キューブは8つの頂点、6つの面、12個の三角形(面ごとに2つ)を持ちます:
// キューブ頂点(各面の頂点ごとに位置+法線)
const CUBE_VERTICES: &[f32] = &[
// 前面 (z = 1)
-1.0, -1.0, 1.0, 0.0, 0.0, 1.0, // 位置, 法線
1.0, -1.0, 1.0, 0.0, 0.0, 1.0,
1.0, 1.0, 1.0, 0.0, 0.0, 1.0,
-1.0, 1.0, 1.0, 0.0, 0.0, 1.0,
// 背面 (z = -1) ...
// 上面 (y = 1) ...
// 全6面について同様
];
const CUBE_INDICES: &[u16] = &[
0, 1, 2, 0, 2, 3, // 前面
4, 5, 6, 4, 6, 7, // 背面
8, 9, 10, 8, 10, 11, // 上面
12, 13, 14, 12, 14, 15, // 底面
16, 17, 18, 16, 18, 19, // 右面
20, 21, 22, 20, 22, 23, // 左面
];GPUへのデータアップロード
fn create_buffer(gl: &GL, data: &[f32]) -> web_sys::WebGlBuffer {
let buffer = gl.create_buffer().unwrap();
gl.bind_buffer(GL::ARRAY_BUFFER, Some(&buffer));
// f32スライスをWebGL用の生バイトに変換
unsafe {
let view = js_sys::Float32Array::view(data);
gl.buffer_data_with_array_buffer_view(
GL::ARRAY_BUFFER, &view, GL::STATIC_DRAW,
);
}
buffer
}レンダリングループ
use std::cell::RefCell;
use std::rc::Rc;
fn start_render_loop(gl: GL, program: WebGlProgram) {
let f = Rc::new(RefCell::new(None));
let g = f.clone();
let angle = Rc::new(RefCell::new(0.0f64));
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let mut a = angle.borrow_mut();
*a += 0.01; // 回転
// クリア
gl.clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT);
// 新しい回転でモデル行列を更新
let model = multiply(&rotate_y(*a), &rotate_x(*a * 0.7));
let model_flat: Vec<f32> = model.iter().flatten().map(|&x| x as f32).collect();
let loc = gl.get_uniform_location(&program, "uModel");
gl.uniform_matrix4fv_with_f32_array(loc.as_ref(), false, &model_flat);
// 描画
gl.draw_elements_with_i32(GL::TRIANGLES, 36, GL::UNSIGNED_SHORT, 0);
// 次のフレームをリクエスト
request_animation_frame(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
request_animation_frame(g.borrow().as_ref().unwrap());
}拡散ライティング
最もシンプルなライティングモデルは、面の法線とライト方向の角度に基づいて明るさを計算します:
ライト ──────────►
θ \ │ 法線
\ │
\ │
\ │
────────────\│────────── 面
*
intensity = max(dot(normal, light_dir), 0.0)
= max(cos(θ), 0.0)
θ = 0°のとき → intensity = 1.0(完全に照らされる)
θ = 90°のとき → intensity = 0.0(エッジオン、暗い)
θ > 90°のとき → intensity = 0.0(反対向き)完全に黒い影を防ぐために環境光を追加します:
final_color = (ambient + diffuse_intensity) * object_color * light_colorWasm + WebGLのパフォーマンスヒント
| テクニック | メリット |
|---|---|
| 行列計算をRustで実行 | 複雑な数学処理がJSより高速 |
| 描画コールのバッチ処理 | GPU状態変更の削減 |
Float32Array::viewの使用 |
GPUへのゼロコピーデータ転送 |
| uniform更新の最小化 | フレームごとに変更分のみ更新 |
| インデックスバッファの使用 | 頂点の重複を削減 |
| 裏面カリング | gl.enable(GL::CULL_FACE) — 見えない三角形をスキップ |
まとめ
- WebGLは低レベルAPI — Rust/Wasmは数学処理(行列演算、物理、プロシージャル生成)に優れている
- 頂点シェーダは3D点を変換し、フラグメントシェーダはピクセルを着色する — どちらもGPU上で実行
- MVP行列 = Projection * View * Model — ローカル座標をスクリーン座標に変換する
- 透視投影は奥行きの錯覚を生み出し、LookAtはカメラを配置する
- 拡散ライティング =
dot(normal, light_direction)— シンプルだが効果的 - WasmからWebGLバッファへのゼロコピーデータアップロードには
js_sys::Float32Array::viewを使用する - レンダリングループはクロージャ経由で
requestAnimationFrameを使用する —Closureがdropされないように保存すること