概述
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);
}
关键点解析
-
glTF 结构理解:
buffers
包含原始二进制数据bufferViews
定义如何解释缓冲区数据accessors
提供类型化和规范化的访问方式meshes
包含实际的几何数据materials
和textures
定义外观
-
数据加载流程:
- 首先加载 glTF JSON 文件
- 然后加载引用的二进制缓冲区和纹理
- 最后解析并初始化 WebGL 资源
-
性能优化建议:
- 使用索引绘制减少顶点处理
- 合并多个 bufferView 减少 WebGL 状态切换
- 使用压缩纹理格式
- 实现视锥剔除和细节层次(LOD)
进阶扩展
-
支持更多 glTF 特性:
- 多材质和纹理
- 骨骼动画
- 变形目标
- PBR 材质
-
使用现有库简化开发:
- Three.js
- Babylon.js
- glTF-Transform
-
性能监控:
- 使用 WebGL 扩展如 EXT_disjoint_timer_query
- 实现帧率统计和内存监控
这个实现提供了加载和显示基本 glTF 模型的核心功能,可以作为更复杂 3D 应用的基础。