10. 透视投影:看起来更真实
前面章节中的的投影,都是正视投影,如你看到的立方体,总是方方正正。这和真实世界的情况有所出入:在真实世界,离我们远的那一面会小一点。这是一种透视投影,本章我们就实现它。
我们需要计算一个透视投影矩阵。幸运的是,已近有人在js中为我们实现好了:mat4的perspective函数。该函数通过y方向上可见区域的夹角、纵横比、和近远距离计算透视投影矩阵。投影矩阵需要配合视图矩阵使用。视图矩阵使用mat4.lookAt函数计算。该函数的前三个参数是三个vec3类型,分别表示眼睛的位置、场景中心的位置和从观察者的角度往上的向量;第四个参数可选,用来保存计算好的矩阵。
着色器中需要用到的矩阵已近比较多:阴影矩阵、旋转矩阵和投影矩阵。如果把它们都作为uniform传递给着色器,这并不是好的做法:一来,你要写n个传值语句;二来,着色器中可以使用的uniform的数目是有限的,因而是宝贵的;三来,你需要在着色器中对这些矩阵作出运算,并且很多时候是重复的运算,这造成了资源浪费,是可耻的。较好的做法是在js中就把各种矩阵算好,算出的结果是一个综合的转换矩阵,在顶点着色器中,只需要简单地将该矩阵和顶点位置相乘即可。
<head>
<meta http-equiv="content-type" content="text/html; charset=gb2312">
<script type="text/javascript" src="glMatrix-0.9.5.js"></script>
<script type="text/javascript" src="glt.js"></script> <!--glt系列函数已转存在glt.js文件中-->
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 v3Position;
uniform mat4 um4ModelView;
varying vec3 v_texCoord;
uniform int uiShadowMode;
void main(void)
{
if(uiShadowMode == 0) v_texCoord = v3Position;
gl_Position = um4ModelView * vec4(v3Position, 1.0);//已近不再计算旋转矩阵、阴影矩阵等;这些矩阵和投影矩阵已经全部合并到um4ModelView中。
}
</script>
<script id="shader-fs" type="x-shader/x-fragment">
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
uniform samplerCube s_texture;
varying vec3 v_texCoord;
uniform int uiShadowMode;
void main(void)
{
if(uiShadowMode == 0) gl_FragColor = textureCube(s_texture, v_texCoord);
else if(uiShadowMode == 1) gl_FragColor = vec4(0.7, 0.7, 0.7, 1.0);
else gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>
<script>
function ShaderSourceFromScript(scriptID)
{
var shaderScript = document.getElementById(scriptID);
if (shaderScript == null) return "";
var sourceCode = "";
var child = shaderScript.firstChild;
while (child)
{
if (child.nodeType == child.TEXT_NODE ) sourceCode += child.textContent;
child = child.nextSibling;
}
return sourceCode;
}
var webgl = null;
var vertexShaderObject = null;
var fragmentShaderObject = null;
var programObject = null;
var cubeBuffer = null;
var cubeIndexBuffer = null;
var v3PositionIndex = 0;
var textureObject = null;
var samplerIndex = -1;
var interval = 300;
var angle = 0;
var um4ModelViewIndex = -1;
var leftKeyDown = false;
var rightKeyDown = false;
var angleX = 0;
var upKeyDown = false;
var downKeyDown = false;
var shadowMat4 = null;//预先计算好的阴影矩阵
var uiShadowModeIndex = -1;
var shadowPlaneBuffer = null;
var mousePosition = [0, 0];
var w = 1;
var h = 1;
var perspectiveMat4 = null;//预先计算好的投影矩阵
function LoadData()
{
var jsCubeData = [
0.3, 0.3, 0.3,
0.3, -0.3, 0.3,
-0.3, -0.3, 0.3,
-0.3, 0.3, 0.3,
0.3, 0.3, -0.3,
0.3, -0.3, -0.3,
-0.3, -0.3, -0.3,
-0.3, 0.3, -0.3
];
cubeBuffer = webgl.createBuffer();
webgl.bindBuffer(webgl.ARRAY_BUFFER, cubeBuffer);
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(jsCubeData), webgl.STATIC_DRAW);
var jsCubeIndex = [
//前
1,2,3,
3,4,1,
//后
5,8,7,
7,6,5,
//左
4,3,7,
7,8,4,
//右
5,6,2,
2,1,5,
//上
5,1,4,
4,8,5,
//下
2,6,7,
7,3,2
];
for(var i=0; i<jsCubeIndex.length; ++i) --jsCubeIndex[i];
cubeIndexBuffer = webgl.createBuffer();
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, cubeIndexBuffer);
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint8Array(jsCubeIndex), webgl.STATIC_DRAW);
textureObject = webgl.createTexture();
webgl.bindTexture(webgl.TEXTURE_CUBE_MAP, textureObject);
webgl.texImage2D(webgl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, webgl.RGB, webgl.RGB, webgl.UNSIGNED_BYTE, document.getElementById('myTexture1'));
webgl.texImage2D(webgl.TEXTURE_CUBE_MAP_NEGATIVE_X, 0, webgl.RGB, webgl.RGB, webgl.UNSIGNED_BYTE, document.getElementById('myTexture2'));
webgl.texImage2D(webgl.TEXTURE_CUBE_MAP_POSITIVE_Y, 0, webgl.RGB, webgl.RGB, webgl.UNSIGNED_BYTE, document.getElementById('myTexture3'));
webgl.texImage2D(webgl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, webgl.RGB, webgl.RGB, webgl.UNSIGNED_BYTE, document.getElementById('myTexture4'));
webgl.texImage2D(webgl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, webgl.RGB, webgl.RGB, webgl.UNSIGNED_BYTE, document.getElementById('myTexture5'));
webgl.texImage2D(webgl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, webgl.RGB, webgl.RGB, webgl.UNSIGNED_BYTE, document.getElementById('myTexture6'));
var jsShadowPlaneData = [-1.0, -1.0, 1.0, -1.0,-0.25,-1.0, 1.0,-0.25,-1.0, 1.0, -1.0, 1.0];
shadowPlaneBuffer = webgl.createBuffer();
webgl.bindBuffer(webgl.ARRAY_BUFFER, shadowPlaneBuffer );
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(jsShadowPlaneData ), webgl.STATIC_DRAW);
shadowMat4 = mat4.create();
gltMakeShadowMatrix([[-1, -1, 1], [1, -1, 1], [0, -0.25, -1]], [-1, 1, 0, 1], shadowMat4);//预先计算好阴影矩阵,这样就不用重复计算
var lookAtMat4 = mat4.create();
mat4.lookAt([0, 0, 1], [0,0,0], [0,1,0], lookAtMat4);
perspectiveMat4 = mat4.create();
mat4.perspective(60, w/h, 0.1, 2, perspectiveMat4);
mat4.multiply(perspectiveMat4, lookAtMat4, perspectiveMat4);
return 0;
}
function RenderScene()
{
webgl.clearColor(0.0, 0.0, 0.0, 1.0);
webgl.clearDepth(1.0);
webgl.clear(webgl.COLOR_BUFFER_BIT|webgl.DEPTH_BUFFER_BIT);
webgl.texParameteri(webgl.TEXTURE_CUBE_MAP, webgl.TEXTURE_MIN_FILTER, webgl.NEAREST);
webgl.texParameteri(webgl.TEXTURE_CUBE_MAP, webgl.TEXTURE_MAG_FILTER, webgl.NEAREST);
webgl.texParameteri(webgl.TEXTURE_CUBE_MAP, webgl.TEXTURE_WRAP_S, webgl.CLAMP_TO_EDGE);
webgl.texParameteri(webgl.TEXTURE_CUBE_MAP, webgl.TEXTURE_WRAP_T, webgl.CLAMP_TO_EDGE);
webgl.activeTexture(webgl.TEXTURE0);
webgl.bindTexture(webgl.TEXTURE_CUBE_MAP, textureObject);
webgl.uniform1i(samplerIndex, 0);
webgl.frontFace(webgl.CW);
webgl.cullFace(webgl.BACK);
//画平面
webgl.disable(webgl.DEPTH_TEST);//相当于地面,禁止深度测试,此时深度缓冲中值保持1.0不变,表示可以被任何其它物体所遮挡
webgl.bindBuffer(webgl.ARRAY_BUFFER, shadowPlaneBuffer);
webgl.enableVertexAttribArray(v3PositionIndex);
webgl.vertexAttribPointer(v3PositionIndex, 3, webgl.FLOAT, false, 0, 0);
webgl.uniform1i(uiShadowModeIndex, 2);
//只需要透视投影
webgl.uniformMatrix4fv(um4ModelViewIndex, false, perspectiveMat4);
webgl.drawArrays(webgl.TRIANGLE_FAN, 0, 4);
var mat4Temp = null;
//画立方体
//先旋转,再透视投影
var m4Rotate = mat4.create();
mat4.identity(m4Rotate);
mat4.rotateZ(m4Rotate, angle*Math.PI/180);
mat4.rotateX(m4Rotate, angleX*Math.PI/180);
webgl.bindBuffer(webgl.ARRAY_BUFFER, cubeBuffer);
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, cubeIndexBuffer);
webgl.enableVertexAttribArray(v3PositionIndex);
webgl.vertexAttribPointer(v3PositionIndex, 3, webgl.FLOAT, false, 0, 0);
//画阴影
webgl.disable(webgl.DEPTH_TEST);
webgl.uniform1i(uiShadowModeIndex, 1);
mat4Temp = mat4.create();
mat4.multiply(shadowMat4, m4Rotate, mat4Temp);
mat4.multiply(perspectiveMat4, mat4Temp, mat4Temp);//perspectiveMat4*(shadowMat4*m4Rotate)
webgl.uniformMatrix4fv(um4ModelViewIndex, false, mat4Temp);
webgl.drawElements(webgl.TRIANGLES, 36, webgl.UNSIGNED_BYTE, 0);
//画立方体
webgl.enable(webgl.DEPTH_TEST);
webgl.depthFunc(webgl.LEQUAL);
webgl.uniform1i(uiShadowModeIndex, 0);
mat4Temp = mat4.create();
mat4.multiply(perspectiveMat4, m4Rotate, mat4Temp);
webgl.uniformMatrix4fv(um4ModelViewIndex, false, mat4Temp);
webgl.drawElements(webgl.TRIANGLES, 36, webgl.UNSIGNED_BYTE, 0);
}
function RotateTriangle()
{
if(leftKeyDown) angle += 10;
if(rightKeyDown) angle -= 10;
if(angle >= 360) angle -= 360;
if(angle < 0) angle += 360;
if(upKeyDown) angleX += 10;
if(downKeyDown) angleX -= 10;
if(angleX >= 360) angleX -= 360;
if(angleX < 0) angleX += 360;
RenderScene();
}
document.onkeydown = function(e)
{
if(e.keyCode == 37) leftKeyDown = true;
if(e.keyCode == 39) rightKeyDown = true;
if(e.keyCode == 38) upKeyDown = true;
if(e.keyCode == 40) downKeyDown = true;
}
document.onkeyup = function(e)
{
if(e.keyCode == 37) leftKeyDown = false;
if(e.keyCode == 39) rightKeyDown = false;
if(e.keyCode == 38) upKeyDown = false;
if(e.keyCode == 40) downKeyDown = false;
}
//这些事件代码仍然只适合FF浏览器
function OnMouseEnter(e)
{
angle = 0;
angleX = 0;
mousePosition = [parseInt(e.screenX), parseInt(e.screenY)];
}
function OnMouseLeave(e)
{
angle = 0;
angleX = 0;
}
function OnMouseMove(e)
{
var x = parseInt(e.screenX);
var y = parseInt(e.screenY);
angle -= (x-mousePosition[0]);//X方向上的位移决定绕Z轴转动的角度
angleX -= (y-mousePosition[1]);//Y方向上的位移决定绕X轴转动的角度
mousePosition = [x, y];
}
function Init()
{
var myCanvasObject = document.getElementById('myCanvas');
myCanvasObject.onmouseenter = OnMouseEnter;
myCanvasObject.onmouseleave = OnMouseLeave;
myCanvasObject.onmousemove = OnMouseMove;
webgl = myCanvasObject.getContext("experimental-webgl");
w = myCanvasObject.clientWidth;
h = myCanvasObject.clientHeight;//将视见区的宽度和高度保存下来,以在后面使用(如计算纵横比)
webgl.viewport(0, 0, w, h);
vertexShaderObject = webgl.createShader(webgl.VERTEX_SHADER);
fragmentShaderObject = webgl.createShader(webgl.FRAGMENT_SHADER);
webgl.shaderSource(vertexShaderObject, ShaderSourceFromScript("shader-vs"));
webgl.shaderSource(fragmentShaderObject, ShaderSourceFromScript("shader-fs"));
webgl.compileShader(vertexShaderObject);
webgl.compileShader(fragmentShaderObject);
if(!webgl.getShaderParameter(vertexShaderObject, webgl.COMPILE_STATUS)){alert(webgl.getShaderInfoLog(vertexShaderObject));return;}
if(!webgl.getShaderParameter(fragmentShaderObject, webgl.COMPILE_STATUS)){alert(webgl.getShaderInfoLog(fragmentShaderObject));return;}
programObject = webgl.createProgram();
webgl.attachShader(programObject, vertexShaderObject);
webgl.attachShader(programObject, fragmentShaderObject);
webgl.bindAttribLocation(programObject, v3PositionIndex, "v3Position");
webgl.linkProgram(programObject);
if(!webgl.getProgramParameter(programObject, webgl.LINK_STATUS)){alert(webgl.getProgramInfoLog(programObject));return;}
samplerIndex = webgl.getUniformLocation(programObject, "s_texture");
um4ModelViewIndex = webgl.getUniformLocation(programObject, "um4ModelView");
uiShadowModeIndex = webgl.getUniformLocation(programObject, "uiShadowMode");
webgl.useProgram(programObject);
if(LoadData() != 0){alert("error:LoadData()!");return;}
window.setInterval("RotateTriangle()", interval);
}
</script>
</head>
<body οnlοad='Init()'>
<canvas id="myCanvas" style="border:1px solid red;" width='600px' height='450px'></canvas><br>
<img id="myTexture1" src='cubeTexture1.bmp'>
<img id="myTexture2" src='cubeTexture2.bmp'>
<img id="myTexture3" src='cubeTexture3.bmp'><br>
<img id="myTexture4" src='cubeTexture4.bmp'>
<img id="myTexture5" src='cubeTexture5.bmp'>
<img id="myTexture6" src='cubeTexture6.bmp'>
</body>
</html>
运行效果如下: