🚀🚀🚀WebGL 加载 glTF 模型

概述

glTF (GL Transmission Format) 是专为 WebGL 和现代图形 API 设计的 3D 模型格式,具有以下优势:

  • 使用 JSON 描述场景结构,易于解析
  • 二进制数据直接可用,无需转换
  • 支持 JPEG/PNG 纹理,便于压缩传输
  • 专为实时渲染优化

完整实现步骤

1. HTML 设置

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>glTF 模型查看器</title>
</head>
<body onload="main()">
  <div><input type='file' id='modelFile' multiple></div>
  <canvas id="webgl" width="800" height="600"></canvas>
  
  <script src="main.js"></script>
</body>
</html>

2. JavaScript 主程序 (main.js)

javascript 复制代码
// WebGL 上下文和着色器程序
let gl, program;

function main() {
  const canvas = document.getElementById('webgl');
  gl = canvas.getContext('webgl');
  
  // 初始化着色器
  initShaders();
  
  // 设置文件输入监听
  setupFileInput();
}

function initShaders() {
  // 顶点着色器
  const vsSource = `
    attribute vec4 a_Position;
    attribute vec2 a_TexCoord;
    varying vec2 v_TexCoord;
    uniform mat4 u_MvpMatrix;
    
    void main() {
      gl_Position = u_MvpMatrix * a_Position;
      v_TexCoord = a_TexCoord;
    }
  `;
  
  // 片段着色器
  const fsSource = `
    precision mediump float;
    uniform sampler2D u_Sampler;
    varying vec2 v_TexCoord;
    
    void main() {
      gl_FragColor = texture2D(u_Sampler, v_TexCoord);
    }
  `;
  
  program = initShaderProgram(gl, vsSource, fsSource);
  gl.useProgram(program);
}

function setupFileInput() {
  const input = document.getElementById('modelFile');
  
  input.addEventListener('change', async (event) => {
    const files = event.target.files;
    const gltfFile = Array.from(files).find(f => f.name.endsWith('.gltf'));
    
    if (!gltfFile) {
      alert('请选择 glTF 文件');
      return;
    }
    
    try {
      // 读取 glTF JSON
      const gltfContent = await readFileAsText(gltfFile);
      const gltf = JSON.parse(gltfContent);
      
      // 加载关联资源
      const resources = await loadGltfResources(gltf, files);
      
      // 初始化模型
      initModel(gltf, resources);
      
    } catch (error) {
      console.error('加载失败:', error);
    }
  });
}

async function loadGltfResources(gltf, files) {
  const resources = {};
  
  // 加载二进制缓冲区
  const bufferInfo = gltf.buffers[0];
  const bufferFile = findFileByName(files, bufferInfo.uri);
  resources.buffer = await readFileAsArrayBuffer(bufferFile);
  
  // 加载纹理
  const imageInfo = gltf.images[0];
  const imageFile = findFileByName(files, imageInfo.uri);
  resources.texture = await loadTexture(imageFile);
  
  return resources;
}

function initModel(gltf, resources) {
  // 初始化顶点缓冲区
  const vertexCount = initVertexBuffers(gl, gltf, resources.buffer);
  
  // 初始化纹理
  initTexture(gl, resources.texture);
  
  // 设置模型视图矩阵
  const mvpMatrix = new Matrix4();
  // ... 设置适当的视图和投影矩阵
  
  // 绘制模型
  drawModel(gl, program, vertexCount, mvpMatrix);
}

3. 辅助函数

javascript 复制代码
// 初始化着色器程序
function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);

  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);

  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    console.error('着色器程序初始化失败:', gl.getProgramInfoLog(shaderProgram));
    return null;
  }

  return shaderProgram;
}

// 加载单个着色器
function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error('着色器编译错误:', gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }

  return shader;
}

// 文件读取辅助函数
function readFileAsText(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsText(file);
  });
}

function readFileAsArrayBuffer(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsArrayBuffer(file);
  });
}

function loadTexture(imageFile) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = () => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.src = reader.result;
    };
    reader.readAsDataURL(imageFile);
  });
}

function findFileByName(files, name) {
  return Array.from(files).find(f => f.name === name);
}

4. 模型初始化函数

javascript 复制代码
function initVertexBuffers(gl, gltf, bufferData) {
  const primitive = gltf.meshes[0].primitives[0];
  
  // 初始化位置属性
  const positionAccessor = gltf.accessors[primitive.attributes.POSITION];
  const positionView = gltf.bufferViews[positionAccessor.bufferView];
  
  const positions = new Float32Array(
    bufferData,
    positionView.byteOffset,
    positionView.byteLength / Float32Array.BYTES_PER_ELEMENT
  );
  
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
  
  const aPosition = gl.getAttribLocation(program, 'a_Position');
  gl.vertexAttribPointer(
    aPosition,
    3, // vec3
    gl.FLOAT,
    false,
    positionView.byteStride || 0,
    positionAccessor.byteOffset || 0
  );
  gl.enableVertexAttribArray(aPosition);
  
  // 初始化纹理坐标属性
  const texCoordAccessor = gltf.accessors[primitive.attributes.TEXCOORD_0];
  const texCoordView = gltf.bufferViews[texCoordAccessor.bufferView];
  
  const texCoords = new Float32Array(
    bufferData,
    texCoordView.byteOffset,
    texCoordView.byteLength / Float32Array.BYTES_PER_ELEMENT
  );
  
  const texCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);
  
  const aTexCoord = gl.getAttribLocation(program, 'a_TexCoord');
  gl.vertexAttribPointer(
    aTexCoord,
    2, // vec2
    gl.FLOAT,
    false,
    texCoordView.byteStride || 0,
    texCoordAccessor.byteOffset || 0
  );
  gl.enableVertexAttribArray(aTexCoord);
  
  // 初始化索引缓冲区
  const indicesAccessor = gltf.accessors[primitive.indices];
  const indicesView = gltf.bufferViews[indicesAccessor.bufferView];
  
  let indices;
  switch (indicesAccessor.componentType) {
    case 5121: // UNSIGNED_BYTE
      indices = new Uint8Array(bufferData, indicesView.byteOffset, indicesView.byteLength);
      break;
    case 5123: // UNSIGNED_SHORT
      indices = new Uint16Array(bufferData, indicesView.byteOffset, indicesView.byteLength / 2);
      break;
    case 5125: // UNSIGNED_INT
      indices = new Uint32Array(bufferData, indicesView.byteOffset, indicesView.byteLength / 4);
      break;
  }
  
  const indexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
  
  return indicesAccessor.count;
}

function initTexture(gl, image) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  
  // 设置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  
  // 上传纹理数据
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  
  // 设置纹理 uniform
  const uSampler = gl.getUniformLocation(program, 'u_Sampler');
  gl.uniform1i(uSampler, 0);
}

function drawModel(gl, program, vertexCount, mvpMatrix) {
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
  // 启用深度测试
  gl.enable(gl.DEPTH_TEST);
  
  // 设置模型视图投影矩阵
  const uMvpMatrix = gl.getUniformLocation(program, 'u_MvpMatrix');
  gl.uniformMatrix4fv(uMvpMatrix, false, mvpMatrix.elements);
  
  // 绘制模型
  gl.drawElements(gl.TRIANGLES, vertexCount, gl.UNSIGNED_SHORT, 0);
}

关键点解析

  1. glTF 结构理解:

    • buffers 包含原始二进制数据
    • bufferViews 定义如何解释缓冲区数据
    • accessors 提供类型化和规范化的访问方式
    • meshes 包含实际的几何数据
    • materials 和 textures 定义外观
  2. 数据加载流程:

    • 首先加载 glTF JSON 文件
    • 然后加载引用的二进制缓冲区和纹理
    • 最后解析并初始化 WebGL 资源
  3. 性能优化建议:

    • 使用索引绘制减少顶点处理
    • 合并多个 bufferView 减少 WebGL 状态切换
    • 使用压缩纹理格式
    • 实现视锥剔除和细节层次(LOD)

进阶扩展

  1. 支持更多 glTF 特性:

    • 多材质和纹理
    • 骨骼动画
    • 变形目标
    • PBR 材质
  2. 使用现有库简化开发:

    • Three.js
    • Babylon.js
    • glTF-Transform
  3. 性能监控:

    • 使用 WebGL 扩展如 EXT_disjoint_timer_query
    • 实现帧率统计和内存监控

这个实现提供了加载和显示基本 glTF 模型的核心功能,可以作为更复杂 3D 应用的基础。

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师3 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学3 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端