转自http://blog.csdn.net/tiewen/article/details/6915899
11. 交互:更好的鼠标控制
在《9. 交互:这次我们用鼠标》中,键盘和鼠标都只控制了立方体绕z轴和x轴的旋转。在从正面观察的情况下,总是无法同时显示立方体的三个面。本章,我们介绍用鼠标拖动实现立方体的3D旋转。
一个旋转对应一个方向向量。如果把所有的方向向量转换为固定的模长,假设为r,并且把向量(x,y,z)当成一个3D坐标,那么,把这些坐标聚集在一起,就形成了一个以(0,0,0)为球心,r为半径的球体。假设我们的鼠标是3D的,那么鼠标在这个球体表面上的每个点击,都将对应一个唯一的3D坐标,并且,该坐标直接和一个方向向量对应。
但问题是,鼠标是2D的。因此,我们需要一种方法,能够将2D的鼠标坐标(x,y)转换为3D的(X,Y,Z)。
我们先反过来看一下,3D球体上的坐标在2D中情况。为了简单和方便解说,我们用正视投影,也不考虑任何变换。上面提及的球体经正视投影到2D坐标系后,生成了一个以(0,0)为圆心,r为半径的圆。也就是说,这个2D的圆,其实是和3D中的球对应的。圆中的任何一点(x,y)都和球体表面某个或某几个(X,Y,Z)对应。想象一下,在2D坐标的(x,y)处(圆的内部或圆周上),画出一条和z轴平行的直线。如果直线和球相切,则在Z=0这个平面上,有一个唯一的交点,该交点的3D坐标可以确定为(x,y,0)。如果该直线和球不相切,则必定和球体有两个交点,一个在+Z范围中,一个在-Z范围中。你可以同时处理这两个点或选取其中任何之一。作为解释,还有本章示例,我选取的是+Z范围中的这个点。因为是正视投影,所以,2D坐标系中的xy轴的值和3D坐标系中的xy轴的值相同,唯一欠缺的是z轴坐标。但我们知道球体有个公式,r2 = x2 + y2 + z2。于是我们可以计算出z为正负sqrt(r2 - x2 - y2)。于是,我们的3D坐标完整了,于是,你想进行实际操作了?先别急。想想实际应用中情况,我们的视见区是(w,h)的大小,是一个矩形;通常也不会把鼠标限定在一个圆形范围。也就是说,鼠标通过拖动进行3D旋转时,其位置可能会超出圆周,如下图所示:
其中,大蓝点表示鼠标相对于坐标系原点的位置(X,Y)。此时,我们需要对它进行处理,一种简单的方法是把它对应到圆周上,如上图中的大红点(直线(0,0)(X,Y)和圆周的交点)。交点的求法真地很简单,任何学过初中数学的人都会,结果已在上图中的下方给出。
如果再仔细思考一下,我们其实可以发现,如果把上图中的圆替换成椭圆,或者说,把前面提到的圆球映射到视见区的椭圆,那么我们就可以更好地处理用户拖动(可以处理更大地拖动范围,更加精确地计算出旋转方向)。如果你足够NB,甚至可以把整个视见区和圆球进行关联。
有了上面的这些知识,我们再看看具体的情况。我们想要的操作过程是,用户鼠标按下,表示开始拖动旋转;按住不放并移动鼠标,就通过移动的位移计算出3D偏移方向;松开鼠标键,表示拖动旋转结束。在这一过程中,我们以鼠标按下时的位置为圆心(它对应初始方向V0(0,0,1));鼠标的位移都相对于该圆心,通过该位移,计算出当前要旋转方向V1。通过V0和V1,我们可以计算出这两个方向之间的夹角α;还可以计算出原点、点V0、点V1所形成的平面的法线Vn。当前的旋转于是可以表示为绕Vn旋转了α角度。
α相关的公式为:V0.V1 = ||V0||.||V1||.cosα
平面法线用现成的js函数gltGetNormalVector获得,或者用V0V1的叉积算得。
在示例中,我暂时去除了键盘的旋转控制,在更久以后的一些综合示例中,会再把它加上。为了方便观看当前的旋转方向,我为立方体的一个表面增加了一个蓝色的圆锥。该圆锥的顶点相对于坐标系原点(0,0,0)。立方体的顶点也相对于坐标系原点(0,0,0)。你可能会奇怪,都是相对于坐标系原点(0,0,0),那它们绘画出来之后难道不会互相覆盖吗?好吧,这是我的错,之前没有介绍。一般,我们对某一物体进行建模,都是使用一个完全独立的坐标系,我们称之为模型坐标系或物体坐标系,或者叫作模型空间或物体空间。在把物体进行组合时,我们需要把物体坐标转换到世界坐标。想象一下盖房子的过程。设计师绘画图纸的时候,绝对不会使用东经、西经和南纬、北纬之类的地球坐标系,而是直接定好比例尺,以某个点为坐标原点。建筑人员看到图纸后,在将要建造房子的地方,会将图纸上的坐标原点对应到适合的地球坐标(东经多少度、北纬多少度)。地球坐标,就是我们3D中的世界坐标,无论你从哪边看,怎么看,它都在那儿,永不改变。能够改变的,是房子的方位以及我们看世界的角度。它们具有对称性:斜着看房子和把房子斜着盖,最终落在我们严重的情景是一样的。回到正题,看看之前的立方体顶点及顶点着色器中的输出位置,非常巧合,立方体的物体坐标系的原点,被映射到世界坐标系的原点;并且,立方体被按1:1的比例生成到世界中。
<html> <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); } </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 if(uiShadowMode == 2) gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); else if(uiShadowMode == 3) gl_FragColor = vec4(0.0, 0.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 um4ModelViewIndex = -1; var shadowMat4 = null;//预先计算好的阴影矩阵 var uiShadowModeIndex = -1; var shadowPlaneBuffer = null; var mouseDown = false; var mousePosition = [0, 0]; var w = 1; var h = 1; var perspectiveMat4 = null;//预先计算好的投影矩阵 var coneBuffer = null; var rotateMat4 = null; var rotatePosV3 = [0, 0, 0];;//记录离圆心的偏移,2D,弄成vec3只是为了方便执行加减操作 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); var jsConeData = [0, 0.2, 0];//圆锥的顶部顶点为(0, 0.2, 0) for(var i=0; i<13; ++i)//计算圆锥顶部圆面上的12个点;圆的半径为0.1 { //保证各个顶点为顺时针顺序 var radians = -i*30*Math.PI/180; jsConeData[jsConeData.length] = parseInt(0.1 * Math.cos(radians)*1000)/1000; jsConeData[jsConeData.length] = 0; jsConeData[jsConeData.length] = -parseInt(0.1 * Math.sin(radians)*1000)/1000; } coneBuffer = webgl.createBuffer(); webgl.bindBuffer(webgl.ARRAY_BUFFER, coneBuffer); webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(jsConeData), webgl.STATIC_DRAW); rotateMat4 = mat4.create(); mat4.identity(rotateMat4); 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; //画立方体 //先旋转,再透视投影 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, rotateMat4, mat4Temp); mat4.multiply(perspectiveMat4, mat4Temp, mat4Temp);//perspectiveMat4*(shadowMat4*rotateMat4) 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, rotateMat4, mat4Temp); 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, 3); mat4Temp = mat4.create(); mat4.identity(mat4Temp); mat4.rotateX(mat4Temp, Math.PI/2);var mat4Temp2 = mat4.create();mat4.identity(mat4Temp2); mat4.translate(mat4Temp2, [0, 0, 0.3]); mat4.multiply(mat4Temp2, mat4Temp, mat4Temp); mat4.multiply(rotateMat4, mat4Temp, mat4Temp); mat4.multiply(perspectiveMat4, mat4Temp, mat4Temp);//perspectiveMat4*(rotateMat4*translateMat4) webgl.uniformMatrix4fv(um4ModelViewIndex, false, mat4Temp); webgl.bindBuffer(webgl.ARRAY_BUFFER, coneBuffer); webgl.enableVertexAttribArray(v3PositionIndex); webgl.vertexAttribPointer(v3PositionIndex, 3, webgl.FLOAT, false, 0, 0); webgl.drawArrays(webgl.TRIANGLE_FAN, 0, 14); } //这些事件代码仍然只适合FF浏览器 function OnMouseDown(e) { mouseDown = true; mousePosition = [parseInt(e.screenX), parseInt(e.screenY)]; } function OnMouseUp(e) { mouseDown = false; vec3.add(rotatePosV3, [parseInt(e.screenX)-mousePosition[0], parseInt(e.screenY)-mousePosition[1], 0]); } function OnMouseMove(e) { if(!mouseDown) return; var tV3 = []; vec3.add(rotatePosV3, [parseInt(e.screenX)-mousePosition[0], parseInt(e.screenY)-mousePosition[1], 0], tV3);//上次偏移加拖动形成的临时偏移成为本次拖动的最终偏移 mat4.identity(rotateMat4);//首先设置为单位矩阵,然后再构造成旋转矩阵 if(tV3[0] == 0 && tV3[1] == 0) return; //根据位移,计算出当前的方向 var v3 = null; var r = Math.min(w, h)/2; var l = Math.sqrt(tV3[0]*tV3[0] + tV3[1] * tV3[1]); if(l <= r) v3 = [tV3[0], tV3[1], Math.sqrt(r*r - l*l)]; else v3 = [r*tV3[0]/l, r*tV3[1]/l, 0]; v3[1] = - v3[1]; var radians = Math.acos(vec3.dot([0,0,1], v3)/r); var vn = []; gltGetNormalVector([0,0,0], [0,0,1], v3, vn);//或vec3.cross([0,0,1], v3, vn); mat4.rotate(rotateMat4, radians, vn); } function Init() { var myCanvasObject = document.getElementById('myCanvas'); myCanvasObject.onmousedown = OnMouseDown; myCanvasObject.onmouseup = OnMouseUp; 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("RenderScene()", 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>
运行效果如下: