webgl-模型加载

1,774 阅读8分钟

前面所有案例中的 3D 模型采用的都是直接给出顶点坐标值((如立方体)或基于数学式用程序生成坐标值(如球体)的方式,这在一些简单的游戏场景中已经足够了。但如果建立一些更复杂、逼真的游戏场景,则物体需要的几何形状可能会很复杂而且不能直接用学公式来描述,如赛车游戏中的车、空战游戏中的战斗机等。

在这种情况下,一般首先用3D建模工具(如3ds Max、Maya、Blender等)建立物体模型,然后导出为特定格式的模型文件并在应用程序中加载渲染。常用的 3D 模型文件格式有obj、3ds、fbx等,这次主要介绍obj模型文件的加载。

obj模型文件概述

obj文件是一种最简单的3D模型文件,其本质上就是文本文件,只是具有固定的格式而已。在obj文件中顶点坐标、三角形面、纹理坐标等信息以固定格式的文本字符串表示,下面给出了一个obj文本的片段。

# Max2Obj Version 4.0 Mar 10th, 2001
……
#
v  4.531507 5.223516 -0.079789
v  4.800519 5.223516 -0.07978
# 2 vertices

vt  4.531507 5.223516 -0.079789
vt  4.800519 5.223516 -0.07978
# 2 texture vertices

vn  4.531507 5.223516 -0.079789
vn  4.481378 5.432536 -0.084661
# 2 vertices normals

g (null)
f 1 8 9
f 9 2 1
# 2 face
g

从上述obj文件片段中可以看出,其内容是以行为基本单位进行组织的,每种以不前缀开头的行有不同的含义,具体情况如下所示。

  1. 以"#"号开头的行为注释,在程序加载的过程中可以忽略它。
  2. 以"v"开头的行作用于存放顶点坐标,其后面的3跟数值分别表示一个顶点的x,y,z坐标。
  3. 以"vt"开头的行作用于存放顶点纹理坐标,其后面的3个数值分别代码纹理坐标的S,T,P分量。
  4. 以"vn开头的行用于存放顶点法向量,其后面的3个数值分别代表一个顶点的法向量在x轴,y轴,z轴上的分量。
  5. 以"g"开头的行表示一组的开始,后面的字符串为此组的名称。所谓组是指由顶点组成的一些面的集合。只包含"g"的行代表一组的结束,以于"g"开头的行形成对应。
  6. 以"f"开头的行表示组中的一个面,如果是三角形则后面有三组用空格分隔的数据,代表三角形的3个顶点。每组数据的包含3个数值,用"/"分隔,依次代表顶点坐标数据索引、顶点纹理坐标索引、顶点向量数据索引。

在obj文件中一般顶点坐标与面的数据是必须选择的,而法向量与纹理数据是可选的。

加载obj文件

1.先介绍下渲染物体的函数。

//加载的用于绘制的3D物体
function ObjObject(gl,vertexDataIn programIn){  //GL上下文//顶点坐标数组//着色器程序对象
    //接收顶点数据
    this.vertexData=vertexDataIn;
    //得到顶点数量
    this.vcount=this.vertexData.length/3;
    //创建顶点数据缓冲
    this.vertexBuffer=gl.createBuffer();
    //将顶点数据送入缓冲
    gl.bindBuffer(gl.ARRAY_BUFFER,this.vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(this.vertexData),gl.STATIC_DRAW);
    //加载着色器程序
    this.program=programIn;
    this.drawSelf=function(ms){
        //送入总矩阵
        var uMVPMatrixHandle=gl.getUniformLocation(this.program, "uMVPMatrix");
        gl.uniformMatrix4fv(uMVPMatrixHandle,false,new Float32Array(ms.getFinalMatrix()));
        //送入变换矩阵
        var uMMatrixHandle=gl.getUniformLocation(this.program, "uMMatrix");
        gl.uniformMatrix4fv(uMMatrixHandle,false,new Float32Array(ms.currMatrix));
        //送入摄像机位置
        var uCameraHandle=gl.getUniformLocation(this.program, "uCamera");
        gl.uniform3fv(uCameraHandle,new Float32Array([ms.cx,ms.cy,ms.cz]));
        //启用顶点数据
        gl.enableVertexAttribArray(gl.getAttribLocation(this.program, "aPosition"));
        //将顶点数据送入渲染管线
        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
        gl.vertexAttribPointer(gl.getAttribLocation(this.program, "aPosition"), 3, gl.FLOAT, false, 0, 0);
        //用顶点法绘制物体
        gl.drawArrays(gl.TRIANGLES, 0, this.vcount);
    }
}

2.开发完绘制加载物体的代码后,接下来将详细介绍读取obj文件请求并对接收的数据进行处理的LoadBall.js.

//请求读取obj文件的方法
function loadObjFile(url){
	var req = new XMLHttpRequest();//创建对服务器的请求
	req.onreadystatechange = function () { processLoadObj(req) };//重写请求的onreadystatechange事件
	req.open("GET", url, true);//设置请求的类型和obj文件的路径
	req.responseType = "text";//设置回应的类型
	req.send(null);
}

function createObj(objDataIn){//用提取的数据创建绘制对象的方法
	if(shaderProgArray[0]){//如果着色器已经进行编译
		ooTri=new ObjObject(gl,objDataIn.vertices,shaderProgArray[0]); //创建绘制用的物体
	}else{//如果着色器还未进行编译
		setTimeout(function(){createObj(objDataIn);},10); //10毫秒之后再次进行调用
	}
}

function processLoadObj(req){//重写后的onreadystatechange事件
	if (req.readyState == 4){ //当接受到服务器传来的数据后
		var objStr = req.responseText;//得到obj文件的文本数据
		var dataTemp=fromObjStrToObjectData(objStr);//对数据进行处理和提取
		createObj(dataTemp);//用提取的数据创建绘制对象
	}
}

3.对接收到的obj数据进行解析处理,提取出顶点坐标数组。

function fromObjStrToObjectData(objStr) {// 原始顶点坐标列表--直接从obj文件中加载
	var alv = new Array().fill(0);// 结果顶点坐标列表--按面组织好
	var alvResult = [];
	var aln = [];// 计算出的法向量坐标
	var alnResult = [];//面索引的列表
	var alFaceIndex = [];
	var setOfNormal = new SetOfNormal();
	var lines = objStr.split("\n");
	for (var lineIndex in lines) {
		var line = lines[lineIndex].replace(/[ \t]+/g, " ").replace(/\s\s*$/, "");
		if (line[0] == "#") {
			continue;
		}
		var array = line.split(" ");
		if (array[0] == "v") {
			alv.push(parseFloat(array[1]));
			alv.push(parseFloat(array[2]));
			alv.push(parseFloat(array[3]));
		} else if (array[0] == "f") {
			var index = new Array(3); //三个顶点索引值的数组
			if (array.length != 4) {
				alert("array.length != 4");
				continue;
			}
			var tempArray = array[1].split("/");
			index[0] = tempArray[0] - 1;
			vx0 = alv[index[0] * 3 + 0];
			vy0 = alv[index[0] * 3 + 1];
			vz0 = alv[index[0] * 3 + 2];
			alvResult.push(vx0);
			alvResult.push(vy0);
			alvResult.push(vz0);
			alFaceIndex.push(index[0]);
			var tempArray = array[2].split("/");
			index[1] = tempArray[0] - 1;
			vx1 = alv[index[1] * 3 + 0];
			vy1 = alv[index[1] * 3 + 1];
			vz1 = alv[index[1] * 3 + 2];
			alvResult.push(vx1);
			alvResult.push(vy1);
			alvResult.push(vz1);
			alFaceIndex.push(index[1]);
			var tempArray = array[3].split("/");
			index[2] = tempArray[0] - 1;
			vx2 = alv[index[2] * 3 + 0];
			vy2 = alv[index[2] * 3 + 1];
			vz2 = alv[index[2] * 3 + 2];
			alvResult.push(vx2);
			alvResult.push(vy2);
			alvResult.push(vz2);
			alFaceIndex.push(index[2]);//记录此面的顶点索引
			var vxa = vx1 - vx0;
			var vya = vy1 - vy0;
			var vza = vz1 - vz0;
			var vxb = vx2 - vx0;
			var vyb = vy2 - vy0;
			var vzb = vz2 - vz0;
			var vNormal = vectorNormal(getCrossProduct(vxa, vya, vza, vxb, vyb, vzb));
			setOfNormal.add(index[0], vNormal);
			setOfNormal.add(index[1], vNormal);
			setOfNormal.add(index[2], vNormal);
		}
	}
	for (i = 0; i < setOfNormal.array.length; i++) {
		var avernormal = new Array(0, 0, 0);
		if (setOfNormal.array[i] != null) {
			for (j = 0; j < setOfNormal.array[i].length; j++) {
				avernormal[0] += (setOfNormal.array[i][j]).nx;
				avernormal[1] += (setOfNormal.array[i][j]).ny;
				avernormal[2] += (setOfNormal.array[i][j]).nz;
			}
			avernormal = vectorNormal(avernormal);
			aln.push(avernormal.nx, avernormal.ny, avernormal.nz);
		}
	}
	for (i = 0; i < alFaceIndex.length; i++) {
		alnResult.push(aln[alFaceIndex[i] * 3], aln[alFaceIndex[i] * 3 + 1], aln[alFaceIndex[i] * 3 + 2]);
	}
	return new ObjectData(alvResult.length / 3, alvResult, alnResult);
}

再编写下着色器代码

#version 300 es
uniform mat4 uMVPMatrix; //总变换矩阵
in vec3 aPosition;  //从渲染管线接收的顶点位置
out vec3 vPosition;  //用于传递给片元着色器的顶点位置
void main() {
   gl_Position = uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制此顶点的位置
   vPosition=aPosition;//将顶点位置传递给片元着色器
}
#version 300 es
precision mediump float;//给出默认浮点精度
in  vec3 vPosition;  //从顶点着色器接收的顶点位置
out vec4 fragColor;//最终片元颜色
void main() {
   vec4 bColor=vec4(0.678,0.231,0.129,1.0);//条纹的颜色(深红色)
   vec4 mColor=vec4(0.763,0.657,0.614,1.0);//间隔区域的颜色(淡红色)
   float y=vPosition.y;//提取顶点的y坐标值
   y=mod((y+100.0)*4.0,4.0);//折算出区间值
   if(y>1.8) {//当区间值大于指定值时
     fragColor = bColor;//设置片元颜色为条纹的颜色
   } else {//当区间值不大于指定值时
     fragColor = mColor;//设置片元颜色为间隔区域的颜色
}}

运行下,效果如下:

image.png 效果不太好看,那我们加入光照和法线的效果。 修改顶点着色器

#version 300 es
uniform mat4 uMVPMatrix; //总变换矩阵
uniform mat4 uMMatrix; //变换矩阵
uniform vec3 uLightLocation;	//光源位置
uniform vec3 uCamera;	//摄像机位置
in vec3 aPosition;  //顶点位置
in vec3 aNormal;    //顶点法向量
out vec4 finalLight;//最终光

//定位光光照计算的方法
void pointLight(					//定位光光照计算的方法
  in vec3 normal,				//法向量
  inout vec4 ambient,			//环境光最终强度
  inout vec4 diffuse,				//散射光最终强度
  inout vec4 specular,			//镜面光最终强度
  in vec3 lightLocation,			//光源位置
  in vec4 lightAmbient,			//环境光强度
  in vec4 lightDiffuse,			//散射光强度
  in vec4 lightSpecular			//镜面光强度
){
  ambient=lightAmbient;			//直接得出环境光的最终强度
  vec3 normalTarget=aPosition+normal;	//计算变换后的法向量
  vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(aPosition,1)).xyz;
  newNormal=normalize(newNormal); 	//对法向量规格化
  //计算从表面点到摄像机的向量
  vec3 eye= normalize(uCamera-(uMMatrix*vec4(aPosition,1)).xyz);
  //计算从表面点到光源位置的向量vp
  vec3 vp= normalize(lightLocation-(uMMatrix*vec4(aPosition,1)).xyz);
  vp=normalize(vp);//格式化vp
  vec3 halfVector=normalize(vp+eye);	//求视线与光线的半向量
  float shininess=5.0;				//粗糙度,越小越光滑
  float nDotViewPosition=max(0.0,dot(newNormal,vp)); 	//求法向量与vp的点积与0的最大值
  diffuse=lightDiffuse*nDotViewPosition;				//计算散射光的最终强度
  float nDotViewHalfVector=dot(newNormal,halfVector);	//法线与半向量的点积
  float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess)); 	//镜面反射光强度因子
  specular=lightSpecular*powerFactor;    			//计算镜面光的最终强度
}


void main()
{
   gl_Position = uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制此顶点位置
   vec4 tempColor=vec4(1.0,1.0,1.0,1.0);
   vec4 ambientTemp, diffuseTemp, specularTemp;   //存放环境光、散射光、镜面反射光的临时变量
   pointLight(normalize(aNormal),ambientTemp,diffuseTemp,specularTemp,uLightLocation,vec4(0.1,0.1,0.1,1.0),vec4(0.7,0.7,0.7,1.0),vec4(0.3,0.3,0.3,1.0));
   finalLight=(ambientTemp+diffuseTemp+specularTemp);
}

修改面片着色器

precision mediump float;
in vec4 finalLight;//最终光
out vec4 fragColor;//输出到的片元颜色
void main(){
   //将计算出的颜色给此片元
      vec3 Color=vec3(0.9,0.9,0.9);
      fragColor = vec4(finalLight.xyz*Color,1.0);//给此片元颜色值
}

添加法线

图1 面片法向量效果图

image.png

在绘制光滑曲面的时候,一个顶点的 法线取相邻的面法线的平均值 顶点法向量效果图

图2

image.png

加载纹理贴图

在现实世界中的物体表面并不一定是纯色的,可能是花纹的,如白瓷的手绘茶壶。

绘制函数添加纹理代码。

 //接收顶点纹理坐标数据
this.vertexTexCoor=vertexTexCoorIn;
//创建顶点纹理坐标缓冲
this.vertexTexCoorBuffer=gl.createBuffer();
//将顶点纹理坐标数据送入缓冲
gl.bindBuffer(gl.ARRAY_BUFFER,this.vertexTexCoorBuffer);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(this.vertexTexCoor),gl.STATIC_DRAW);	   

对应解析obj文件增加接受vt的代码。 运行效果如下:

image.png 同理,上面的法线是通过我们计算获取的,在导出obj是可生产法线数据,可通过解析代替计算。**

双面光照效果

开启背面裁剪效果

 gl.enable(gl.CULL_FACE);

image.png 关闭背面裁剪效果

 gl.disable(gl.CULL_FACE);

image.png

对应的webgl的gl_FrontFacing变量可提供面片是正面还是背面,可因此对背面进行而外处理,实现内外不同的颜色亮度的效果。