欢迎光临
我们一直在努力

glTF WebGPU 查看器

一个基于 WebGPU 和 Rust (WASM) 构建的高性能 glTF 2.0 查看器。 在线地址:https://gltf-wgpu-viewer.scorp.fun/ Github

选择Rust来编译 WebAssembly (WASM) 是一个在现代高性能 Web 开发中非常流行的选择,特别是在像gltf-wgpu-viewer这样的图形和3D项目中。 导入模型和相关资源后的效果: 在这里插入图片描述

为什么选择Rust来编译 WASM

  • 极致的性能 (Performance)
    • 接近原生的执行速度 :Rust 编译生成的WASM代码运行速度非常快,接近C/C++的原生性能。
    • 无垃圾回收 (No GC) :与JavaScript、Go或C#不同,Rust没有运行时垃圾回收器。这意味着它不会因为GC暂停而导致帧率波动(Jank),这对3D渲染和游戏至关重要。
    • 适合计算密集型任务 :在项目中,Rust被用于glTF解析 ( src/lib.rs )。解析大型3D模型文件涉及大量的二进制数据读取、结构体转换和内存操作,Rust在这方面比JavaScript快得多且更节省内存 。
  • 内存安全与可靠性 (Memory Safety)
    • 编译期保障 :Rust的所有权(Ownership)和借用(Borrowing)机制在编译阶段就消除了空指针、数据竞争和内存泄漏等常见错误。
    • 稳定性 :对于复杂的解析逻辑(如处理glTF的各种嵌套结构和二进制buffer),Rust能够保证代码在运行时不会因为内存问题莫名崩溃,提供了比C++更高的安全性。
  • 优秀的 WASM 工具链支持(Rust拥有目前最成熟的WASM开发生态)
    • wasm-bindgen:你的项目使用了这个库,它能自动生成JavaScript和Rust之间的“胶水代码”,让你可以像调用普通JS函数一样调用Rust函数,并且能高效地在两者之间传递复杂数据(如字符串、数组、对象)。
    • wasm-pack:你使用的构建工具,一键集成编译、打包、发布流程,体验非常顺滑。
  • 功能特性

    • WebGPU 后端:利用现代 WebGPU API 进行高效渲染。
    • Rust WASM 解析器:使用 Rust 和 wasm-bindgen 实现高性能 glTF 解析。
    • Draco 压缩支持:通过 Google Draco Decoder 支持 KHR_draco_mesh_compression 扩展。
    • 交互式控制:支持轨道相机的旋转、平移和缩放。
    • 拖拽上传:支持直接将 .gltf/.glb 文件或文件夹拖入窗口。
    • PBR 渲染:基础 PBR 光照模型(环境光、方向光、镜面光)。

    主要目录结构

    .
    ├── src/lib.rs # Rust 核心逻辑 (glTF 解析)
    ├── main.js # WebGPU 渲染主循环
    ├── camera.js # 相机控制逻辑
    ├── index.html # UI 入口
    └── Cargo.toml # Rust 依赖配置

    关键依赖 (Cargo.toml)

    • wasm-bindgen: 用于 Rust 与 JS 交互。
    • js-sys: 操作 JS 的原生类型(如 Float32Array)。
    • gltf: Rust 生态中最成熟的 glTF 解析库。
    • glam / nalgebra: (可选) 用于数学计算,本项目主要使用原生数组以减少依赖。

    Rust 侧:构建 glTF 解析器

    文件位置:src/lib.rs

    定义跨语言数据结构 (MeshData)

    为了让 JS 能高效获取解析后的数据,我们需要定义一个 wasm_bindgen 导出的结构体。

    关键点:零拷贝 (Zero-Copy) 访问 我们不将数据 clone 给 JS,而是通过 unsafe 块返回 Rust 内存的视图 (View)。

    #[wasm_bindgen]
    pub struct MeshData {
    positions: Vec<f32>,
    indices: Vec<u32>,
    // … 其他属性
    }

    #[wasm_bindgen]
    impl MeshData {
    // JS 调用 mesh.positions 时,直接返回指向 WASM 内存的 Float32Array
    #[wasm_bindgen(getter)]
    pub fn positions(&self) -> Float32Array {
    unsafe { Float32Array::view(&self.positions) }
    }
    }

    处理多文件加载 (fetch_file_from_js)

    glTF 文件通常引用外部的 .bin 或纹理文件。由于 WASM 无法直接访问文件系统,我们需要通过 JS 传入的文件映射来获取数据。

    实现逻辑:

  • JS 将所有拖入的文件读取为 文件名 -> Uint8Array 的 Map。

  • Rust 解析 glTF 时遇到外部引用(URI),调用辅助函数在 Map 中查找。

  • 智能路径匹配:为了兼容不同系统的路径分隔符和 URL 编码,实现了多种查找策略(如解码 URL、去除前缀 ./、仅匹配文件名等)。

    // src/lib.rs
    fn fetch_file_from_js(file_map: &JsValue, uri: &str) -> Option<Vec<u8>> {
    // 1. 尝试直接使用 URI 获取
    if let Some(v) = get_js_file(file_map, uri) { return Some(v); }

    // 2. 尝试 URL 解码
    let decoded = urlencoding::decode(uri).ok()?;
    if let Some(v) = get_js_file(file_map, &decoded) { return Some(v); }

    // 3. 尝试去除路径前缀 (处理相对路径 ./texture.png)
    let uri_trim = uri.strip_prefix("./").unwrap_or(uri);
    if let Some(v) = get_js_file(file_map, uri_trim) { return Some(v); }

    // 4. 回退到仅文件名匹配 (解决目录结构不匹配问题)
    let uri_base = uri_trim.rsplit(|c| c == '/' || c == '\\\\').next()?;
    if let Some(v) = get_js_file(file_map, uri_base) { return Some(v); }

    None
    }

  • 解析主逻辑 (parse_multifile_gltf)

  • 加载主文件:从 Map 中提取 .gltf 内容。
  • 解析 JSON:使用 gltf::Gltf::from_slice 解析结构。
  • 遍历图元 (Primitives):
    • 遍历每个 Mesh 的 Primitive。
    • 读取 POSITION (位置), NORMAL (法线), TEXCOORD_0 (UV) 等访问器 (Accessor)。
    • 将二进制数据转换为扁平的 Vec<f32> 或 Vec<u32>。

    // src/lib.rs
    for mesh in gltf.meshes() {
    for primitive in mesh.primitives() {
    let reader = primitive.reader(|buffer| {
    // 回调:根据 buffer 索引查找对应的二进制数据
    // files 是预先加载好的 HashMap<String, Vec<u8>>
    // 注意:这里简化了逻辑,实际需处理 source 映射
    Some(&files.get(&buffer.index().to_string())?[..])
    });

    if let Some(iter) = reader.read_positions() {
    for vertex in iter {
    // 扁平化存储 positions
    positions.push(vertex[0]);
    positions.push(vertex[1]);
    positions.push(vertex[2]);
    }
    }
    // … 同理读取 normals, uvs, indices
    }
    }

  • 合并数据:将多个 Mesh 的数据合并到一个大的 Vertex Buffer 中(为了简化 Draw Call),并记录 draw_calls 信息(索引偏移量、计数)。
  • JS 侧:WebGPU 渲染管线搭建

    文件位置:main.js

    初始化 WebGPU

    WebGPU 的初始化是异步的:

  • navigator.gpu.requestAdapter(): 获取物理 GPU。
  • adapter.requestDevice(): 获取逻辑设备。
  • context.configure(): 绑定 Canvas 并设置格式(通常是 bgra8unorm)。
  • 编写 WGSL 着色器

    顶点着色器 (Vertex Shader):

    • 接收:位置、法线、UV。
    • 计算:应用 MVP 矩阵 (uniforms.mvp * vec4(pos, 1.0))。
    • 输出:裁剪空间位置 + 世界空间属性(用于光照)。

    片段着色器 (Fragment Shader):

    • 实现光照模型:
      • 环境光:基础亮度。
      • 漫反射 (Diffuse):dot(normal, light_dir)。
      • 镜面光 (Specular):Blinn-Phong 模型。
    • 纹理采样:使用 textureSample 读取颜色。// main.js (WGSL)
      @fragment
      fn fs_main(in: vOut) -> @location(0) vec4<f32> {
      // 采样基础颜色
      let base = textureSample(t_diffuse, s_diffuse, in.uv).rgb;

      // 计算法线与视线
      let N = normalize(in.normal);
      let V = normalize(uniforms.camera_pos – in.world_pos);

      // 1. 头灯漫反射 (光线方向 = 视线方向)
      let diffuse = max(dot(N, V), 0.0) * 0.8;

      // 2. 镜面高光 (Blinn-Phong)
      let H = normalize(V + V); // 半向量 (因为光线方向=视线方向)
      let specular = pow(max(dot(N, H), 0.0), 32.0) * 0.2;

      return vec4<f32>(base * (0.3 + diffuse) + vec3(specular), 1.0);
      }

    创建渲染管线 (RenderPipeline)

    管线对象定义了渲染的所有状态,创建后不可修改(Immutable):

    • Layout: auto (自动推断 BindGroup 布局)。
    • Vertex State: 关联 Shader 模块,定义 Buffer 布局(Stride, Offset, Format)。
    • Fragment State: 关联 Shader 模块,定义输出颜色格式。
    • DepthStencil: 开启深度测试 (depthCompare: 'less'),防止模型前后遮挡错误。

    // main.js
    const pipeline = device.createRenderPipeline({
    layout: 'auto',
    vertex: {
    module: device.createShaderModule({ code: shaderCode }),
    entryPoint: 'vs_main',
    buffers: [
    // 顶点缓冲布局:Pos(3) + Normal(3) + Color(4) + UV(2)
    // Stride = 12 bytes (3 * 4 bytes)
    { arrayStride: 12, attributes: [{ shaderLocation: 0, format: 'float32x3', offset: 0 }] },
    // … 其他属性配置
    ]
    },
    primitive: {
    topology: 'triangle-list',
    cullMode: 'none' // 禁用剔除以支持双面材质
    },
    depthStencil: {
    depthWriteEnabled: true,
    depthCompare: 'less',
    format: 'depth24plus'
    }
    });

    核心逻辑:数据桥接与资源上传

    这是连接 Rust 与 WebGPU 的桥梁。

    数据流向

  • 用户操作:拖拽文件夹 -> JS 读取所有文件为 ArrayBuffer。
  • Rust 解析:调用 parse_multifile_gltf -> 返回 MeshData 对象。
  • GPU 上传:// 从 Rust 获取视图
    const positionsView = meshData.positions;

    // 创建 GPU Buffer
    const vBuffer = device.createBuffer({
    size: positionsView.byteLength,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
    });

    // 写入数据
    device.queue.writeBuffer(vBuffer, 0, positionsView);
    注意:这里直接把 Rust 内存写入了 GPU,非常高效。

  • 渲染循环 (Render Loop)

  • CommandEncoder:开始记录 GPU 命令。
  • RenderPass:设置背景色 (ClearOp) 和深度缓冲区。
  • Pipeline 绑定:setPipeline。
  • 资源绑定:
    • setBindGroup(0): 绑定 Uniforms (MVP 矩阵)。
    • setBindGroup(1): 绑定材质 (纹理 + 采样器)。
  • 绘制:
    • setVertexBuffer: 绑定顶点数据。
    • setIndexBuffer: 绑定索引数据。
    • drawIndexed: 根据 Rust 返回的 index_count 执行绘制。
  • 高级特性:Draco 压缩支持

    对于大型 3D 模型,Google Draco 压缩可以显著减小文件体积(通常减少 10-20 倍),从而加快网络加载速度。虽然解压需要时间,但对于 Web 应用,带宽往往是更大的瓶颈。

    混合解析策略

    本项目实现了“双路解析”机制:

    • 标准 glTF:优先使用 Rust (gltf crate) 解析,速度最快。
    • Draco glTF:当检测到 KHR_draco_mesh_compression 扩展时,自动切换到 JS 端的 Draco 解码器。

    注意:虽然 Rust 也有 Draco 绑定,但在 WASM 环境下编译 C++ 依赖较为复杂,因此直接复用 Google 官方提供的 draco_decoder.wasm 是更轻量、更稳健的方案。

    实现细节 (JS 端)

  • 动态加载解码器: 使用 draco_wasm_wrapper.js 动态加载 Google CDN 上的 draco_decoder.wasm,避免增加主包体积。

    // main.js
    const dracoBaseUrl = 'https://www.gstatic.com/draco/versioned/decoders/1.5.7/';

    async function getDracoModule() {
    if (dracoModulePromise) return dracoModulePromise;
    // 动态插入 script 标签加载解码器
    const script = document.createElement('script');
    script.src = `${dracoBaseUrl}draco_wasm_wrapper.js`;
    // … Promise 封装加载逻辑
    }

  • 场景图扁平化 (Flattening): Draco 解码器解出的是局部坐标系的几何体。为了适配我们简单的渲染管线,必须在 CPU 端手动应用场景图变换:

    • 遍历节点树:递归计算每个节点的世界矩阵 (WorldMatrix = ParentMatrix * LocalMatrix)。
    • 预变换顶点:v_world = WorldMatrix * v_local。
    • 预变换法线:n_world = InverseTranspose(WorldMatrix) * n_local (使用逆转置矩阵以维持法线垂直性)。

    // 递归遍历场景图
    function traverse(nodeIndex, parentMatrix) {
    const node = json.nodes[nodeIndex];
    const localMatrix = mat4.create();
    getNodeMatrix(node, localMatrix); // 获取 T/R/S 变换

    // 计算世界矩阵
    const worldMatrix = mat4.create();
    mat4.multiply(worldMatrix, parentMatrix, localMatrix);

    if (node.mesh !== undefined) {
    // 解码网格并应用世界矩阵变换
    const primitives = getDecodedMesh(node.mesh);
    for (const prim of primitives) {
    // … 变换位置 positions

    // 计算法线矩阵 (逆转置)
    const normalMatrix = mat4.create();
    mat4.invert(normalMatrix, worldMatrix);
    mat4.transpose(normalMatrix, normalMatrix);
    // … 变换法线 normals
    }
    }

    // 递归子节点
    if (node.children) {
    node.children.forEach(child => traverse(child, worldMatrix));
    }
    }

  • 数据统一: Draco 解码后的数据被重组为与 Rust 解析器完全一致的 MeshData 格式(Positions, Indices, DrawCalls),从而复用后续的 GPU 上传和渲染逻辑。

  • 交互系统:轨道相机实现

    文件位置:camera.js

    数学基础

    使用 gl-matrix 库进行矩阵运算。

    • View Matrix (视图矩阵):使用 mat4.lookAt(eye, target, up) 生成。
    • Projection Matrix (投影矩阵):使用 mat4.perspective 生成。
    • MVP: Projection * View * Model。

    事件处理

    • 旋转:监听 pointermove。计算 dx, dy 差值,更新球坐标系角度 (Yaw, Pitch)。
    • 平移:监听 Shift + 拖拽。计算视图矩阵的 Right 和 Up 向量,在相机平面上移动 target 点。
    • 缩放:监听 wheel。修改相机与目标的距离 distance。

    数据更新

    每帧或交互发生时:

  • JS 计算新的 MVP 矩阵。
  • 使用 device.queue.writeBuffer 更新 Uniform Buffer。
  • 下一帧渲染时 Shader 自动使用新矩阵。
  • // main.js
    function updateMVP(device, buffer, canvas, camera, bounds) {
    // 1. 计算投影矩阵 (自动适配长宽比)
    const projection = mat4.create();
    mat4.perspective(projection, Math.PI / 4, canvas.width / canvas.height, near, far);

    // 2. WebGPU Z轴校正 (OpenGL [-1, 1] -> WebGPU [0, 1])
    const correction = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,0.5,0, 0,0,0.5,1]);
    mat4.multiply(projection, correction, projection);

    // 3. 计算视图矩阵 (LookAt)
    const view = camera.getViewMatrix();

    // 4. 合成 MVP 并上传
    const mvp = mat4.create();
    mat4.multiply(mvp, projection, view);

    // 写入 Uniform Buffer (offset 0 = MVP, offset 64 = CameraPos)
    device.queue.writeBuffer(buffer, 0, mvp);
    device.queue.writeBuffer(buffer, 64, new Float32Array(camera.getEyePosition()));
    }

    赞(0)
    未经允许不得转载:171主机测评 » glTF WebGPU 查看器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址