交互:更好的鼠标控制

xiaoxiao2021-02-28  82

 

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>

运行效果如下:

 

转载请注明原文地址: https://www.6miu.com/read-52504.html

最新回复(0)