You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WebGL加载OBJ模型无显示问题排查及代码求助

WebGL加载OBJ模型无法显示问题排查与修复

问题现象

开发WebGL项目时,尝试加载并显示简单OBJ模型,但模型始终无法渲染。控制台出现OBJ关键字未处理警告,以及WebGL顶点数据不匹配的错误。打印顶点数据长度显示为36,与OBJ文件一致,但WebGL提示顶点获取所需数据与属性提供的不匹配。

控制台报错信息

Unhandled Model Keyword: mtllib at line 1 loadModel.js:85:21
Unhandled Model Keyword: o at line 2 loadModel.js:85:21
Unhandled Model Keyword: usemtl at line 37 loadModel.js:85:21
Unhandled Model Keyword: s at line 38 loadModel.js:85:21
<empty string> app.js:68:13
<empty string> app.js:74:13
WebGL warning: drawArraysInstanced: Vertex fetch requires 36, but attribs only supply 26. 32
After reporting 32, no further warnings will be reported for this WebGL context.

相关代码

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
    <link href="index.css" rel="stylesheet">
    <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap" rel="stylesheet">
    
    <script src="gl-matrix-min.js" defer></script>
    <script type="module" src="app.js" defer></script>
</head>
<body>
    <canvas></canvas>
    <script>
        const c = document.getElementsByTagName("canvas")[0];
        c.width = window.innerWidth;
        c.height = window.innerHeight;
    </script>
</body>
</html>

App.js

import "./gl-matrix-min.js";
const { mat2, mat2d, mat4, mat3, quat, quat2, vec2, vec3, vec4 } = glMatrix;

import { loadTexture } from "./loadTexture.js"
import { parseOBJ } from "./loadModel.js";
import { loadTextResource, repeat } from "./util.js";

window.onload = function (){
    loadTextResource('/Shaders/shader.vs.glsl', function(vsErr, vsText){
        if(vsErr){
            alert("Error getting vertex shader (check console for more info)");
            console.error(vsErr);
        }else{
            loadTextResource('/Shaders/shader.fs.glsl', function(fsErr, fsText){
                if(fsErr){
                    alert("Error getting fragment shader (check console for more info)");
                    console.error(fsErr);
                }else{
                    RunScene(vsText, fsText);
                }
            });
        }
    });
}
async function RunScene(vsText, fsText){
    const canvas = document.querySelector('canvas');
    const gl = canvas.getContext('webgl');

    if(!gl){
        console.error("WebGL not supported on this browser!");
    }

    const response = await fetch('Models/test.obj');
    const text = await response.text();
    const data = parseOBJ(text);

    //Vertex Data
    const vertexData = data.positions;

    // Image UV Data
    const uvData = data.texCoords;

    // F|L|B|R|T|U
    const normalData = data.normals;

//    console.log(normalData);

    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW);

    const uvBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(uvData), gl.STATIC_DRAW);

    const normalBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normalData), gl.STATIC_DRAW);

    const picture = loadTexture(gl, `Textures/Pablo.png`);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, picture);

    const vertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vertexShader, vsText);
    gl.compileShader(vertexShader);
    console.log(gl.getShaderInfoLog(vertexShader));


    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fragmentShader, fsText);
    gl.compileShader(fragmentShader);
    console.log(gl.getShaderInfoLog(fragmentShader));

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

    const positionLocation = gl.getAttribLocation(program, 'position');
    gl.enableVertexAttribArray(positionLocation);
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

    const uvLocation = gl.getAttribLocation(program, 'uv');
    gl.enableVertexAttribArray(uvLocation);
    gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
    gl.vertexAttribPointer(uvLocation, 2, gl.FLOAT, false, 0, 0);

    const normalLocation = gl.getAttribLocation(program, 'normal');
    gl.enableVertexAttribArray(normalLocation);
    gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
    gl.vertexAttribPointer(normalLocation, 3, gl.FLOAT, false, 0, 0);

    gl.useProgram(program);
    gl.enable(gl.DEPTH_TEST);

    const uniformLocations = {
        matrix: gl.getUniformLocation(program, 'matrix'),
        normalMatrix: gl.getUniformLocation(program, 'normalMatrix'),
        textureLocation: gl.getUniformLocation(program, 'textureID'),
    };


    const modelMatrix = mat4.create();
    const viewMatrix = mat4.create();
    const projectionMatrix = mat4.create();
    mat4.perspective(projectionMatrix, 75 * Math.PI/180, canvas.width/canvas.height, 1e-4, 1e4);

    const mvMatrix = mat4.create();
    const mvpMatrix = mat4.create();

    const normalMatrix = mat4.create();

    mat4.translate(viewMatrix, viewMatrix, [0, 0.01, 2]);
    mat4.invert(viewMatrix, viewMatrix);

    gl.uniform1i(uniformLocations.textureLocation, 0);

    function animate(){
        requestAnimationFrame(animate);

        mat4.rotateX(modelMatrix, modelMatrix, Math.PI/100);
        mat4.rotateY(modelMatrix, modelMatrix, Math.PI/200);

        mat4.multiply(mvMatrix, viewMatrix, modelMatrix);
        mat4.multiply(mvpMatrix, projectionMatrix, mvMatrix);
        
        mat4.invert(normalMatrix, mvMatrix);
        mat4.transpose(normalMatrix, normalMatrix);
        
        gl.uniformMatrix4fv(uniformLocations.matrix, false, mvpMatrix);
        gl.uniformMatrix4fv(uniformLocations.normalMatrix, false, normalMatrix);

        gl.drawArrays(gl.TRIANGLES, 0, vertexData.length/3);
    }

    animate();
}

loadModel.js

export function parseOBJ(text){
    const objPositions = [[0, 0, 0]];
    const objTexCoords = [[0, 0]];
    const objNormals = [[0, 0, 0]];

    const objVertexData = [
        objPositions,
        objTexCoords,
        objNormals,
    ];
    
    let webglVertexData = [
        [], //Verts
        [], //UV
        [], //Normals
    ];

    function addVertex(vert) {
        const ptn = vert.split('/');
        ptn.forEach((objIndexStr, i) => {
            if (!objIndexStr) {
                return;
            }
            const objIndex = parseInt(objIndexStr);
            const dataArray = objVertexData[i];
    
            if (dataArray && dataArray.length > 0) {
                let index = objIndex > 0 ? objIndex - 1 : dataArray.length + objIndex;
                
                // Ensure the index is within bounds
                index = Math.max(0, Math.min(index, dataArray.length - 1));
    
                const data = dataArray[index];
                if (data !== undefined) {
                    webglVertexData[i].push(...dataArray[index]);
                } else {
                    console.error(`Error: Undefined data at index ${index} for objVertexData[${i}]`);
                }
            } else {
                console.error(`Error: No data found for objVertexData[${i}]`);
            }
        });
    }
    
    const keywords = {
        v(parts){
            objPositions.push(parts.map(parseFloat));
        },
        vn(parts){
            objTexCoords.push(parts.map(parseFloat));
        },
        vt(parts){
            objNormals.push(parts.map(parseFloat));
        },
        f(parts){
            const numTriangles = parts.length - 2;
            for(let tri = 0; tri < numTriangles; ++tri){
                addVertex(parts[0]);
                addVertex(parts[tri + 1]);
                addVertex(parts[tri + 2]);
            }
        }
    };

    const keywordRE = /(\w*)(?: )*(.*)/;
    const lines = text.split('\n');
    for(let lineNum = 0; lineNum < lines.length; ++lineNum){
        const line = lines[lineNum].trim();
        if(line === '' || line.startsWith('#')){
            continue;
        }

        const m = keywordRE.exec(line);
        if(!m){
            continue;
        }
        
        const [, keyword, unparsedArgs] = m;
        const parts = line.split(/\s+/).slice(1);
        const handler = keywords[keyword];
        if(!handler){
            console.warn('Unhandled Model Keyword:', keyword, 'at line', lineNum + 1);
            continue;
        }
        handler(parts, unparsedArgs);
    }

    return {
        positions: webglVertexData[0],
        texCoords: webglVertexData[1],
        normals: webglVertexData[2]
    };
}

问题根源与修复方案

1. 核心错误:OBJ关键字映射颠倒

在loadModel.js的keywords对象中,vn(法线)和vt(纹理坐标)的处理逻辑完全颠倒:

  • 原本vn应该处理法线数据,却错误地往objTexCoords中添加数据
  • 原本vt应该处理纹理坐标,却错误地往objNormals中添加数据

这导致UV数据和法线数据被交换,WebGL获取顶点属性时,实际数据的数量与预期不匹配,引发报错。

修复后的keywords对象:

const keywords = {
    v(parts){
        objPositions.push(parts.map(parseFloat));
    },
    vn(parts){
        objNormals.push(parts.map(parseFloat)); // 修正:vn对应法线
    },
    vt(parts){
        objTexCoords.push(parts.map(parseFloat)); // 修正:vt对应UV坐标
    },
    f(parts){
        const numTriangles = parts.length - 2;
        for(let tri = 0; tri < numTriangles; ++tri){
            addVertex(parts[0]);
            addVertex(parts[tri + 1]);
            addVertex(parts[tri + 2]);
        }
    },
    // 添加以下空处理消除未处理关键字警告
    mtllib() {},
    o() {},
    usemtl() {},
    s() {}
};

2. 消除未处理关键字警告

添加mtllib、o、usemtl、s的空处理函数,避免控制台输出警告信息,这些关键字不影响模型渲染,仅用于OBJ文件的材质和分组定义。

3. 验证顶点数据一致性

修复后,重新打印data.positions、data.texCoords、data.normals的长度,确保三者的顶点数量一致(每个顶点对应一组位置、UV、法线数据),WebGL就能正确获取所有顶点属性,渲染出模型。

内容的提问来源于stack exchange,提问作者Homemade Tech

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 16:23:10