案例查看地址:点击这里
迄今为止,示例程序都是在代码中显式定位三维模型的顶点坐标,并保存在Float32Array类型的数组中。然而,大部分三维程序都是从模型文件中读取三维模型的顶点坐标和颜色数据,而模型文件是有三维建模软件生成的。
程序需要从模型文件中读取数据,并保存在之前使用的那些数组和缓冲区中。具体地,程序需要:
(1)准备Float32Array类型的数组vertices,从文件中读取模型的顶点坐标数据并保存到其中。
(2)准备Float32Array类型的数组 colors,从文件中读取模型的顶点颜色数据并保存到其中。
(3)准备Float32Array类型的数组 normals, 从文件中读取模型的顶点法线数据并保存到其中。
(4)准备 Uint15Array(或Uint8Array)类型的数组 indices, 从文件中读取顶点索引数据并保存在其中,顶点索引数据定义了组成整个模型的三角形序列。
(5)将前4步获取的数据写入缓冲区中,调用()以绘制出整个立方体。
为了能将模型数据读入相应的数组中,并在第5步绘制它,我们需要理解obj文件格式中每一行的含义。
# Blender v2.60 (sub 0) OBJ File: ''
#
mtllib
o Cube
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
v 1.000000 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
usemtl Material
f 1 2 3 4
f 5 8 7 6
f 2 6 7 3
f 3 7 8 4
f 5 1 4 8
usemtl Material.001
f 1 5 6 2
OBJ文件格式
OBJ格式的文件由若干个部分组成,包括顶点坐标部分、表面定义部分、材质定义部分等。每个部分定义了多个顶点、法线、表面等等。
(1)以井号(#)开头的行表示注释,上面1行和2行就是软件根据自身版本创建出来的注释。
(2)第3行引用了一个外部材质文件。OBJ格式将模型的材质信息存储在外部的MTL格式的文件中。
mtllib < 外部材质文件名 >这里,外部材质文件是。
(3)第4行按照如下格式指定了模型的名称:
o < 模型名称 > 示例程序中没有用到这条信息
(4)第5行到第12行按照如下格式定义了顶点的坐标,其中w是可选的,如果没有就默认为1.0 。
v x y z [w]本例中的模型时一个标准的立方体,共有8个顶点。
(5)第13行到第20行先指定了某个材质,然后列举了使用这个材质的表面。第13行指定了材质名称,该材质被定义在第3行引用的MTL文件中。
usemtl < 材质名 >
(6)接下来的第14行到第18行定义了使用这个材质的表面。每个表面是由顶点、纹理坐标和法线的索引序列定义的。
f v1 v2 v3 v4 ···
其中v1、v2、v3、v4是之前定义的顶点的索引值。注意,这里顶点的索引值从1开始,而不是从0开始。本例为了简单,没有包含法线,如果包含了法线向量,就需要遵照如下格式:
f v1//vn1 v2//vn2 v3//vn3 ···
其中,vn1、vn2等式法线向量的索引值,也是从1开始。
(7)第19行到第20行定义了使用了另一个材质的表面,即橘黄色的表面。
MTL 文件格式
# Blender MTL File: ''
# Material Count: 2
newmtl Material
Ka 0.000000 0.000000 0.000000
Kd 1.000000 0.000000 0.000000
Ks 0.000000 0.000000 0.000000
Ns 96.078431
Ni 1.000000
d 1.000000
illum 0
newmtl Material.001
Ka 0.000000 0.000000 0.000000
Kd 1.000000 0.450000 0.000000
Ks 0.000000 0.000000 0.000000
Ns 96.078431
Ni 1.000000
d 1.000000
illum 0
(1)第1行和第2行是注释。
(2)第3行使用newmtl定义一个新材质,格式如下:
newmtl < 材质名 >
材质名被OBJ文件引用,如材质Material就在的第13行被引用了。
(3)第4行到第6行,分别使用Ka、Kd和Ks定义了表面的环境色、漫射色和高光色。颜色使用RGB格式定义,每个分量值的区间为[0.0,1.0],本例只用到漫反射,也就是物体表面本来的颜色。我们不用管其他两个颜色的含义。
(4)第7行使用Ns指定了高光色的权重,第8行用Ni指定了表面光学密度,第9行使用d指定了透明度,第10行用illum指定了光照模型。本例没有用到这些信息。
(5)第11行到第18行以同样的方法定义了另一种材质Material.001 。
理解了OBJ文件格式和Mtl文件格式,就可以从文件中读取模型各表面顶点坐标、颜色、法向量和索引信息,组织成数组写入缓冲区对象,调用drawElements()以绘制出模型。这里,OBJ对象没有定义法线方向,但我们可以根据顶点坐标,通过叉乘操作计算出法线方向。
案例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Title</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
margin: 0;
display: block;
}
</style>
</head>
<body οnlοad="main()">
<canvas height="800" width="800"></canvas>
</body>
<script src="lib/"></script>
<script src="lib/"></script>
<script src="lib/"></script>
<script src="lib/"></script>
<script>
//设置WebGL全屏显示
var canvas = ("canvas");
= ;
= ;
//顶点着色器
var vertexShaderSource = "" +
"attribute vec4 a_Position;\n" +
"attribute vec4 a_Color;\n" +
"attribute vec4 a_Normal;\n" +
"uniform mat4 u_MvpMatrix;\n" +
"uniform mat4 u_NormalMatrix;\n" +
"varying vec4 v_Color;\n" +
"void main(){\n" +
" vec3 lightDirection = vec3(-0.35, 0.35, 0.87);\n" +
" gl_Position = u_MvpMatrix * a_Position;\n" +
" vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n" +
" float nDotL = max(dot(normal, lightDirection), 0.0);\n" +
" v_Color = vec4(a_Color.rgb * nDotL, a_Color.a);\n" +
"}\n";
//片元着色器
var fragmentShaderSource = "" +
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"varying vec4 v_Color;\n" +
"void main(){\n" +
" gl_FragColor = v_Color;\n" +
"}\n";
function main() {
var canvas = ("canvas");
var gl = getWebGLContext(canvas);
if(!gl){
("无法获取WebGL的上下文");
return;
}
//初始化着色器
if(!initShaders(gl, vertexShaderSource, fragmentShaderSource)){
("无法初始化片元着色器");
return;
}
//设置背景色和隐藏面消除
(0.2, 0.2, 0.2, 1.0);
(gl.DEPTH_TEST);
//获取着色器相关的attribute和uniform变量
var program = ;
program.a_Position = (program, "a_Position");
program.a_Color = (program, "a_Color");
program.a_Normal = (program, "a_Normal");
program.u_MvpMatrix = (program, "u_MvpMatrix");
program.u_NormalMatrix = (program, "u_NormalMatrix");
if(program.a_Position < 0 || program.a_Color < 0 || program.a_Normal < 0 || !program.u_MvpMatrix || !program.u_NormalMatrix){
("无法获取到attribute和uniform相关变量");
return;
}
//为顶点坐标、颜色和法向量准备空白缓冲区对象
var model = initVertexBuffer(gl, program);
if(!model){
("无法准备空白缓冲区");
return;
}
//计算视点投影矩阵
var viewProjectMatrix = new Matrix4();
(30.0, /, 1.0, 5000.0);
(0.0, 500.0, 200.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
//读取OBJ文件
readOBJFile("resources/", gl, model, 60, true);
var currentAngle = 0.0; //当前模型的旋转角度
var tick = function () {
currentAngle = animate(currentAngle); //更新角度
draw(gl, program, currentAngle, viewProjectMatrix, model);
requestAnimationFrame(tick);
};
tick();
}
//声明变换矩阵(模型矩阵,模型视图投影矩阵,正常矩阵)
var g_modelMatrix = new Matrix4();
var g_mvpMatrix = new Matrix4();
var g_normalMatrix = new Matrix4();
//绘制当前的模型
function draw(gl, program, angle, viewProjectMatrix, model) {
//判断obj文件和mtl文件都已经解析完成
if(g_objDoc !== null && g_objDoc.isMTLComplete()){
g_drawingInfo = onReadComplete(gl, model, g_objDoc);
g_objDoc = null;
}
//判断模型数据是否解析完成
if(!g_drawingInfo) return;
(gl.COLOR_BUFFER_BIT, gl.DEPTH_BUFFER_BIT);
//设置模型旋转
g_modelMatrix.setRotate(angle, 1.0, 0.0, 0.0);
g_modelMatrix.rotate(angle, 0.0, 1.0, 0.0);
g_modelMatrix.rotate(angle, 0.0, 0.0, 1.0);
//计算正常的变换矩阵并将值赋值给u_NormalMatrix
g_normalMatrix.setInverseOf(g_modelMatrix);
g_normalMatrix.transpose();
gl.uniformMatrix4fv(program.u_NormalMatrix, false, g_normalMatrix.elements);
//计算模型视图投影矩阵
g_mvpMatrix.set(viewProjectMatrix);
g_mvpMatrix.multiply(g_modelMatrix);
gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.elements);
//绘制
(, g_drawingInfo., gl.UNSIGNED_SHORT, 0);
}
//创建缓冲区对象并初始化配置
function initVertexBuffer(gl, program) {
var obj = new Object();
= createEmptyArrayBuffer(gl, program.a_Position, 3, );
= createEmptyArrayBuffer(gl, program.a_Normal, 3, );
= createEmptyArrayBuffer(gl, program.a_Color, 4, );
= ();
if(! || ! || ! || !){
return null;
}
(gl.ARRAY_BUFFER, null);
return obj;
}
//创建一个缓冲区对象,将其分配给属性变量,并启用赋值
function createEmptyArrayBuffer(gl, a_attribute, num, type) {
var buffer = ();
if(!buffer){
("无法创建缓冲区");
return null;
}
(gl.ARRAY_BUFFER, buffer);
(a_attribute, num, type, false, 0, 0);
(a_attribute);
return buffer;
}
function readOBJFile(filename, gl, model, scale, reverse) {
var request = new XMLHttpRequest();
("GET", filename, true);
();
= function () {
if( === 4 && == 200){
//获取到数据调用方法处理
onReadOBJFile(, filename, gl, model, scale, reverse);
}
}
}
var g_objDoc = null; //obj文件的信息数据
var g_drawingInfo = null; //绘制3d模型的相关数据
//obj文件读取成功后开始解析
function onReadOBJFile(fileString, fileName, gl, obj, scale, reverse) {
var objDoc = new OBJDoc(fileName); // 创建一个OBJDoc 对象
var result = (fileString, scale, reverse); //解析文件
if(!result){
g_objDoc = null;
g_drawingInfo = null;
("obj文件解析错误");
return;
}else {
//解析成功赋值给g_objDoc
g_objDoc = objDoc;
}
}
//obj文件已经呗成功读取解析后处理函数
function onReadComplete(gl, model, objDoc) {
//从OBJ文件获取顶点坐标和颜色
var drawingInfo = ();
//将数据写入缓冲区
("数据开始");
("顶点坐标",);
("法向量",);
("颜色",);
("索引值",);
//顶点
(gl.ARRAY_BUFFER, );
(gl.ARRAY_BUFFER, , gl.STATIC_DRAW);
//法向量
(gl.ARRAY_BUFFER, );
(gl.ARRAY_BUFFER, , gl.STATIC_DRAW);
//颜色
(gl.ARRAY_BUFFER, );
(gl.ARRAY_BUFFER, , gl.STATIC_DRAW);
//索引值
(gl.ELEMENT_ARRAY_BUFFER, );
(gl.ELEMENT_ARRAY_BUFFER, , gl.STATIC_DRAW);
return drawingInfo;
}
//模型角度改变函数
var angle_step = 30;
var last = +new Date();
function animate(angle) {
var now = +new Date();
var elapsed = now - last;
last = now;
var newAngle = angle + (angle_step*elapsed)/1000.0;
return newAngle%360;
}
//------------------------------------------------------------------------------
// OBJParser
//------------------------------------------------------------------------------
// OBJDoc object
// Constructor
var OBJDoc = function(fileName) {
= fileName;
= new Array(0); // Initialize the property for MTL
= new Array(0); // Initialize the property for Object
= new Array(0); // Initialize the property for Vertex
= new Array(0); // Initialize the property for Normal
}
// Parsing the OBJ file
= function(fileString, scale, reverse) {
var lines = ('\n'); // Break up into lines and store them as array
(null); // Append null
var index = 0; // Initialize index of line
var currentObject = null;
var currentMaterialName = "";
// Parse line by line
var line; // A string in the line to be parsed
var sp = new StringParser(); // Create StringParser
while ((line = lines[index++]) != null) {
(line); // init StringParser
var command = (); // Get command
if(command == null) continue; // check null command
switch(command){
case '#':
continue; // Skip comments
case 'mtllib': // Read Material chunk
var path = (sp, );
var mtl = new MTLDoc(); // Create MTL instance
(mtl);
var request = new XMLHttpRequest();
= function() {
if ( == 4) {
if ( != 404) {
onReadMTLFile(, mtl);
}else{
= true;
}
}
}
('GET', path, true); // Create a request to acquire the file
(); // Send the request
continue; // Go to the next line
case 'o':
case 'g': // Read Object name
var object = (sp);
(object);
currentObject = object;
continue; // Go to the next line
case 'v': // Read vertex
var vertex = (sp, scale);
(vertex);
continue; // Go to the next line
case 'vn': // Read normal
var normal = (sp);
(normal);
continue; // Go to the next line
case 'usemtl': // Read Material name
currentMaterialName = (sp);
continue; // Go to the next line
case 'f': // Read face
var face = (sp, currentMaterialName, , reverse);
(face);
continue; // Go to the next line
}
}
return true;
}
= function(sp, fileName) {
// Get directory path
var i = ("/");
var dirPath = "";
if(i > 0) dirPath = (0, i+1);
return dirPath + (); // Get path
}
= function(sp) {
var name = ();
return (new OBJObject(name));
}
= function(sp, scale) {
var x = () * scale;
var y = () * scale;
var z = () * scale;
return (new Vertex(x, y, z));
}
= function(sp) {
var x = ();
var y = ();
var z = ();
return (new Normal(x, y, z));
}
= function(sp) {
return ();
}
= function(sp, materialName, vertices, reverse) {
var face = new Face(materialName);
// get indices
for(;;){
var word = ();
if(word == null) break;
var subWords = ('/');
if( >= 1){
var vi = parseInt(subWords[0]) - 1;
(vi);
}
if( >= 3){
var ni = parseInt(subWords[2]) - 1;
(ni);
}else{
(-1);
}
}
// calc normal
var v0 = [
vertices[[0]].x,
vertices[[0]].y,
vertices[[0]].z];
var v1 = [
vertices[[1]].x,
vertices[[1]].y,
vertices[[1]].z];
var v2 = [
vertices[[2]].x,
vertices[[2]].y,
vertices[[2]].z];
// 面の法線を計算してnormalに設定
var normal = calcNormal(v0, v1, v2);
// 法線が正しく求められたか調べる
if (normal == null) {
if ( >= 4) { // 面が四角形なら別の3点の組み合わせで法線計算
var v3 = [
vertices[[3]].x,
vertices[[3]].y,
vertices[[3]].z];
normal = calcNormal(v1, v2, v3);
}
if(normal == null){ // 法線が求められなかったのでY軸方向の法線とする
normal = [0.0, 1.0, 0.0];
}
}
if(reverse){
normal[0] = -normal[0];
normal[1] = -normal[1];
normal[2] = -normal[2];
}
= new Normal(normal[0], normal[1], normal[2]);
// Devide to triangles if face contains over 3 points.
if( > 3){
var n = - 2;
var newVIndices = new Array(n * 3);
var newNIndices = new Array(n * 3);
for(var i=0; i<n; i++){
newVIndices[i * 3 + 0] = [0];
newVIndices[i * 3 + 1] = [i + 1];
newVIndices[i * 3 + 2] = [i + 2];
newNIndices[i * 3 + 0] = [0];
newNIndices[i * 3 + 1] = [i + 1];
newNIndices[i * 3 + 2] = [i + 2];
}
= newVIndices;
= newNIndices;
}
= ;
return face;
}
// Analyze the material file
function onReadMTLFile(fileString, mtl) {
var lines = ('\n'); // Break up into lines and store them as array
(null); // Append null
var index = 0; // Initialize index of line
// Parse line by line
var line; // A string in the line to be parsed
var name = ""; // Material name
var sp = new StringParser(); // Create StringParser
while ((line = lines[index++]) != null) {
(line); // init StringParser
var command = (); // Get command
if(command == null) continue; // check null command
switch(command){
case '#':
continue; // Skip comments
case 'newmtl': // Read Material chunk
name = (sp); // Get name
continue; // Go to the next line
case 'Kd': // Read normal
if(name == "") continue; // Go to the next line because of Error
var material = (sp, name);
(material);
name = "";
continue; // Go to the next line
}
}
= true;
}
// Check Materials
= function() {
if( == 0) return true;
for(var i = 0; i < ; i++){
if(![i].complete) return false;
}
return true;
}
// Find color by material name
= function(name){
for(var i = 0; i < ; i++){
for(var j = 0; j < [i].; j++){
if([i].materials[j].name == name){
return([i].materials[j].color)
}
}
}
return(new Color(0.8, 0.8, 0.8, 1));
}
//------------------------------------------------------------------------------
// Retrieve the information for drawing 3D model
= function() {
// Create an arrays for vertex coordinates, normals, colors, and indices
var numIndices = 0;
for(var i = 0; i < ; i++){
numIndices += [i].numIndices;
}
var numVertices = numIndices;
var vertices = new Float32Array(numVertices * 3);
var normals = new Float32Array(numVertices * 3);
var colors = new Float32Array(numVertices * 4);
var indices = new Uint16Array(numIndices);
// Set vertex, normal and color
var index_indices = 0;
for(var i = 0; i < ; i++){
var object = [i];
for(var j = 0; j < ; j++){
var face = [j];
var color = ();
var faceNormal = ;
for(var k = 0; k < ; k++){
// Set index
indices[index_indices] = index_indices;
// Copy vertex
var vIdx = [k];
var vertex = [vIdx];
vertices[index_indices * 3 + 0] = ;
vertices[index_indices * 3 + 1] = ;
vertices[index_indices * 3 + 2] = ;
// Copy color
colors[index_indices * 4 + 0] = ;
colors[index_indices * 4 + 1] = ;
colors[index_indices * 4 + 2] = ;
colors[index_indices * 4 + 3] = ;
// Copy normal
var nIdx = [k];
if(nIdx >= 0){
var normal = [nIdx];
normals[index_indices * 3 + 0] = ;
normals[index_indices * 3 + 1] = ;
normals[index_indices * 3 + 2] = ;
}else{
normals[index_indices * 3 + 0] = ;
normals[index_indices * 3 + 1] = ;
normals[index_indices * 3 + 2] = ;
}
index_indices ++;
}
}
}
return new DrawingInfo(vertices, normals, colors, indices);
}
//------------------------------------------------------------------------------
// MTLDoc Object
//------------------------------------------------------------------------------
var MTLDoc = function() {
= false; // MTL is configured correctly
= new Array(0);
}
= function(sp) {
return (); // Get name
}
= function(sp, name) {
var r = ();
var g = ();
var b = ();
return (new Material(name, r, g, b, 1));
}
//------------------------------------------------------------------------------
// Material Object
//------------------------------------------------------------------------------
var Material = function(name, r, g, b, a) {
= name;
= new Color(r, g, b, a);
}
//------------------------------------------------------------------------------
// Vertex Object
//------------------------------------------------------------------------------
var Vertex = function(x, y, z) {
= x;
= y;
= z;
}
//------------------------------------------------------------------------------
// Normal Object
//------------------------------------------------------------------------------
var Normal = function(x, y, z) {
= x;
= y;
= z;
}
//------------------------------------------------------------------------------
// Color Object
//------------------------------------------------------------------------------
var Color = function(r, g, b, a) {
= r;
= g;
= b;
= a;
}
//------------------------------------------------------------------------------
// OBJObject Object
//------------------------------------------------------------------------------
var OBJObject = function(name) {
= name;
= new Array(0);
= 0;
}
= function(face) {
(face);
+= ;
}
//------------------------------------------------------------------------------
// Face Object
//------------------------------------------------------------------------------
var Face = function(materialName) {
= materialName;
if(materialName == null) = "";
= new Array(0);
= new Array(0);
}
//------------------------------------------------------------------------------
// DrawInfo Object
//------------------------------------------------------------------------------
var DrawingInfo = function(vertices, normals, colors, indices) {
= vertices;
= normals;
= colors;
= indices;
}
//------------------------------------------------------------------------------
// Constructor
var StringParser = function(str) {
; // Store the string specified by the argument
; // Position in the string to be processed
(str);
}
// Initialize StringParser object
= function(str){
= str;
= 0;
}
// Skip delimiters
= function() {
for(var i = , len = ; i < len; i++){
var c = (i);
// Skip TAB, Space, '(', ')
if (c == '\t'|| c == ' ' || c == '(' || c == ')' || c == '"') continue;
break;
}
= i;
}
// Skip to the next word
= function() {
();
var n = getWordLength(, );
+= (n + 1);
}
// Get word
= function() {
();
var n = getWordLength(, );
if (n == 0) return null;
var word = (, n);
+= (n + 1);
return word;
}
// Get integer
= function() {
return parseInt(());
}
// Get floating number
= function() {
return parseFloat(());
}
// Get the length of word
function getWordLength(str, start) {
var n = 0;
for(var i = start, len = ; i < len; i++){
var c = (i);
if (c == '\t'|| c == ' ' || c == '(' || c == ')' || c == '"')
break;
}
return i - start;
}
//------------------------------------------------------------------------------
// Common function
//------------------------------------------------------------------------------
function calcNormal(p0, p1, p2) {
// v0: a vector from p1 to p0, v1; a vector from p1 to p2
var v0 = new Float32Array(3);
var v1 = new Float32Array(3);
for (var i = 0; i < 3; i++){
v0[i] = p0[i] - p1[i];
v1[i] = p2[i] - p1[i];
}
// The cross product of v0 and v1
var c = new Float32Array(3);
c[0] = v0[1] * v1[2] - v0[2] * v1[1];
c[1] = v0[2] * v1[0] - v0[0] * v1[2];
c[2] = v0[0] * v1[1] - v0[1] * v1[0];
// Normalize the result
var v = new Vector3(c);
();
return ;
}
</script>
</html>
里面引入了一个文件读取组件,具体的不做介绍了。因为如果文件加载显示,还是做的比较优秀,到学习再进行解释。