🚀🚀🚀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 包含实际的几何数据
    • materialstextures 定义外观
  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 应用的基础。

相关推荐
掘金酱几秒前
🔥 稀土掘金 x Trae 夏日寻宝之旅火热进行ing:做任务赢大疆pocket3、Apple watch等丰富大礼
前端·后端·trae
1024小神几秒前
tauri项目添加多文件下载功能,并支持下载进度回调显示在前端页面上
前端·javascript
Ace_31750887761 分钟前
义乌购拍立淘API接入指南
前端
不想说话的麋鹿7 分钟前
《NestJS 实战:RBAC 系统管理模块开发 (四)》:用户绑定
前端·后端·全栈
我是谁谁20 分钟前
JavaScript 中的 Map、WeakMap、Set 详解
前端
laperter31 分钟前
vue3项目第三篇
前端
呆呆的心34 分钟前
深入剖析 JavaScript 数据类型与 Symbol 类型的独特魅力😃
前端·javascript·面试
嘉小华35 分钟前
Kotlin委托机制详解
前端
有仙则茗37 分钟前
process.cwd()和__dirname有什么区别
前端·javascript·node.js
我是谁谁1 小时前
JavaScript 闭包应用场景详解
前端