|
| 1 | + |
| 2 | +/** |
| 3 | +* A class for controlling the projection and |
| 4 | +* view of a 3D scene, in the nature of an abstract "camera". |
| 5 | +* @class |
| 6 | +* @alias Camera |
| 7 | +* @param {glutil.Scene} A 3D scene to associate with this |
| 8 | +* camera object. |
| 9 | +* @param {number} fov Vertical field of view, in degrees. Should be less |
| 10 | +* than 180 degrees. (The smaller |
| 11 | +* this number, the bigger close objects appear to be.) |
| 12 | +* @param {number} near The distance from the camera to |
| 13 | +* the near clipping plane. Objects closer than this distance won't be |
| 14 | +* seen. This should be slightly greater than 0. |
| 15 | +* @param {number} far The distance from the camera to |
| 16 | +* the far clipping plane. Objects beyond this distance will be too far |
| 17 | +* to be seen. |
| 18 | +*/ |
| 19 | +function Camera(scene, fov, nearZ, farZ){ |
| 20 | + this.target=[0,0,0]; // target position |
| 21 | + this.distance=Math.max(nearZ,10); // distance from the target in units |
| 22 | + this.position=[0,0,this.distance]; |
| 23 | + this.angleQuat=GLMath.quatIdentity(); |
| 24 | + this.angleX=0; |
| 25 | + this.angleY=0; |
| 26 | + this.scene=scene; |
| 27 | + this.fov=fov; |
| 28 | + this.near=nearZ; |
| 29 | + this.far=farZ; |
| 30 | + if(typeof InputTracker!="undefined"){ |
| 31 | + this.input=new InputTracker(scene.getContext().canvas); |
| 32 | + } |
| 33 | + this.currentAspect=this.scene.getAspect(); |
| 34 | + this.scene.setPerspective(this.fov,this.currentAspect,this.near,this.far); |
| 35 | + this._updateLookAt(); |
| 36 | +} |
| 37 | +Camera._vec3diff=function(a,b){ |
| 38 | + return [a[0]-b[0],a[1]-b[1],a[2]-b[2]]; |
| 39 | +} |
| 40 | +/** |
| 41 | +* Moves the camera to the left or right, adjusting |
| 42 | +* its angle, while maintaining its distance to the target |
| 43 | +* position. |
| 44 | +* @param {number} angleDegrees Angle, in degrees, |
| 45 | +* to move the camera. Positive angles mean left, |
| 46 | +* negative angles mean right. |
| 47 | +* @return {Camera} This object. |
| 48 | +*/ |
| 49 | +Camera.prototype.moveAngleHorizontal=function(angleDegrees){ |
| 50 | + if(angleDegrees!=0){ |
| 51 | + this._angleHorizontal(-angleDegrees); |
| 52 | + this._resetPosition(); |
| 53 | + } |
| 54 | + return this; |
| 55 | +} |
| 56 | +/** |
| 57 | + * Not documented yet. |
| 58 | + * @param {*} angleDegrees |
| 59 | + *//** @private */ |
| 60 | +Camera.prototype._angleHorizontal=function(angleDegrees){ |
| 61 | + this.angleY+=(angleDegrees*GLMath.PiDividedBy180)%(Math.PI*2); |
| 62 | + if(this.angleY<0)this.angleY=(Math.PI*2+this.angleY); |
| 63 | + this.angleQuat=GLMath.quatFromEuler( |
| 64 | + this.angleX*GLMath.Num180DividedByPi, |
| 65 | + this.angleY*GLMath.Num180DividedByPi, |
| 66 | + 0, |
| 67 | + GLMath.PitchYawRoll); |
| 68 | +} |
| 69 | +/** |
| 70 | + * Not documented yet. |
| 71 | + * @param {*} angleDegrees |
| 72 | + *//** @private */ |
| 73 | +Camera.prototype._angleVertical=function(angleDegrees){ |
| 74 | + var oldangle=this.angleX; |
| 75 | + this.angleX+=(angleDegrees*GLMath.PiDividedBy180)%(Math.PI*2); |
| 76 | + if(this.angleX<0)this.angleX=(Math.PI*2+this.angleX); |
| 77 | + this.angleQuat=GLMath.quatFromEuler( |
| 78 | + this.angleX*GLMath.Num180DividedByPi, |
| 79 | + this.angleY*GLMath.Num180DividedByPi, |
| 80 | + 0, |
| 81 | + GLMath.PitchYawRoll); |
| 82 | +} |
| 83 | + |
| 84 | +/** |
| 85 | +* Moves the camera up or down, adjusting its angle, while |
| 86 | +* maintaining its distance to the target position. |
| 87 | +* The camera won't turn to 90 degrees above or beyond, |
| 88 | +* or to 90 degrees below or beyond. |
| 89 | +* @param {number} angleDegrees Angle, in degrees, |
| 90 | +* to move the camera. Positive angles mean down, |
| 91 | +* negative angles mean up. |
| 92 | +* @return {Camera} This object. |
| 93 | +*/ |
| 94 | +Camera.prototype.moveAngleVertical=function(angleDegrees){ |
| 95 | + if(angleDegrees!=0){ |
| 96 | + this._angleVertical(-angleDegrees); |
| 97 | + this._resetPosition(); |
| 98 | + } |
| 99 | + return this; |
| 100 | +} |
| 101 | + |
| 102 | +/** |
| 103 | +* Turns the camera up or down, maintaining |
| 104 | +* its current position. |
| 105 | +* @param {number} angleDegrees Angle, in degrees, |
| 106 | +* to move the camera. Positive angles mean down, |
| 107 | +* negative angles mean up. |
| 108 | +* @return {Camera} This object. |
| 109 | +*/ |
| 110 | +Camera.prototype.turnVertical=function(angleDegrees){ |
| 111 | + if(angleDegrees!=0){ |
| 112 | + var pos=this.getPosition(); |
| 113 | + this._angleVertical(angleDegrees); |
| 114 | + this._resetTarget(); |
| 115 | + } |
| 116 | + return this; |
| 117 | +} |
| 118 | +/** @private */ |
| 119 | +Camera.prototype._resetPosition=function(){ |
| 120 | + var diff=Camera._vec3diff(this.target,this.position); |
| 121 | + var dist=GLMath.vec3length(diff); |
| 122 | + var newVector=GLMath.quatTransform(this.angleQuat,[0,0,-dist]); |
| 123 | + this.position[0]=this.target[0]-newVector[0]; |
| 124 | + this.position[1]=this.target[1]-newVector[1]; |
| 125 | + this.position[2]=this.target[2]-newVector[2]; |
| 126 | + this._updateLookAt(); |
| 127 | + this.setDistance(this.distance); |
| 128 | +} |
| 129 | +/** @private */ |
| 130 | +Camera.prototype._resetTarget=function(){ |
| 131 | + var diff=Camera._vec3diff(this.target,this.position); |
| 132 | + var dist=GLMath.vec3length(diff); |
| 133 | + var newVector=GLMath.quatTransform(this.angleQuat,[0,0,-dist]); |
| 134 | + this.target[0]=this.position[0]+newVector[0]; |
| 135 | + this.target[1]=this.position[1]+newVector[1]; |
| 136 | + this.target[2]=this.position[2]+newVector[2]; |
| 137 | + this._updateLookAt(); |
| 138 | + this.setDistance(this.distance); |
| 139 | +} |
| 140 | + |
| 141 | +/** |
| 142 | +* Turns the camera to the left or right, maintaining |
| 143 | +* its current position. |
| 144 | +* @param {number} angleDegrees Angle, in degrees, |
| 145 | +* to move the camera. Positive angles mean right, |
| 146 | +* negative angles mean left. |
| 147 | +* @return {Camera} This object. |
| 148 | +*/ |
| 149 | +Camera.prototype.turnHorizontal=function(angleDegrees){ |
| 150 | + if(angleDegrees!=0){ |
| 151 | + var pos=this.getPosition(); |
| 152 | + this._angleHorizontal(-angleDegrees); |
| 153 | + this._resetTarget(); |
| 154 | + } |
| 155 | + return this; |
| 156 | +} |
| 157 | + |
| 158 | +/** |
| 159 | +* Gets the current position of the camera. |
| 160 | +* @return {Camera} A 3-element array giving |
| 161 | +* the X, Y, and Z coordinates, respectively, of |
| 162 | +* the camera's position. |
| 163 | +*/ |
| 164 | +Camera.prototype.getPosition=function(){ |
| 165 | + return this.position.slice(0,3); |
| 166 | +} |
| 167 | + |
| 168 | +Camera.prototype.setDistance=function(dist){ |
| 169 | + if(dist<0)throw new Error("invalid distance") |
| 170 | + if(dist<this.near)dist=this.near; |
| 171 | + var diff=Camera._vec3diff(this.target,this.position); |
| 172 | + var curdist=GLMath.vec3length(diff); |
| 173 | + var distdiff=curdist-diff; |
| 174 | + this.distance=dist; |
| 175 | + if(distdiff!=0){ |
| 176 | + var factor=dist/curdist; |
| 177 | + this.position[0]=this.target[0]-diff[0]*factor; |
| 178 | + this.position[1]=this.target[1]-diff[1]*factor; |
| 179 | + this.position[2]=this.target[2]-diff[2]*factor; |
| 180 | + this._updateLookAt(); |
| 181 | + } |
| 182 | + return this; |
| 183 | +} |
| 184 | + |
| 185 | +/** |
| 186 | +* Moves the camera's position to the given |
| 187 | +* X, Y, and Z coordinates, and adjusts the target's |
| 188 | +* position to maintain the current distance and |
| 189 | +* orientation. |
| 190 | +* @param {number} X coordinate. |
| 191 | +* @param {number} Y coordinate. |
| 192 | +* @param {number} Z coordinate. |
| 193 | +* @return {Camera} This object. |
| 194 | +*/ |
| 195 | +Camera.prototype.movePosition=function(cx, cy, cz){ |
| 196 | + if(cx!=0 && cy!=0 && cz!=0){ |
| 197 | + var pos=this.getPosition(); |
| 198 | + this.target[0]+=(cx-pos[0]); |
| 199 | + this.target[1]+=(cy-pos[1]); |
| 200 | + this.target[2]+=(cz-pos[2]); |
| 201 | + this.position=[cx,cy,cz]; |
| 202 | + } |
| 203 | + return this; |
| 204 | +} |
| 205 | + |
| 206 | +/** |
| 207 | +* Moves the camera closer or farther to the target |
| 208 | +* position. The camera won't move closer than the distance |
| 209 | +* to the near clipping plane. |
| 210 | +* @param {number} distance Distance, in world |
| 211 | +* space units, to move the camera. Positive values |
| 212 | +* mean closer, negative angles mean farther. |
| 213 | +* @return {Camera} This object. |
| 214 | +*/ |
| 215 | +Camera.prototype.moveClose=function(dist){ |
| 216 | + if(dist!=0){ |
| 217 | + var diff=Camera._vec3diff(this.target,this.position); |
| 218 | + var realDist=GLMath.vec3length(diff); |
| 219 | + var newDist=realDist-dist; |
| 220 | + if(newDist<this.near){ |
| 221 | + newDist=this.near; // too close |
| 222 | + } |
| 223 | + this.distance=newDist; |
| 224 | + var factor=newDist/realDist; |
| 225 | + this.position[0]=this.target[0]-diff[0]*factor; |
| 226 | + this.position[1]=this.target[1]-diff[1]*factor; |
| 227 | + this.position[2]=this.target[2]-diff[2]*factor; |
| 228 | + this._updateLookAt(); |
| 229 | + } |
| 230 | + return this; |
| 231 | +} |
| 232 | + |
| 233 | +/** |
| 234 | +* Moves the target and the camera either forward or back, |
| 235 | +* maintaining the distance between the camera and the target. |
| 236 | +* @param {number} distance Distance, in world |
| 237 | +* space units, to move the target and camera, either forward |
| 238 | +* or back. Positive values |
| 239 | +* cause the camera to move forward, negative angles |
| 240 | +* cause it to move back. |
| 241 | +* @return {Camera} This object. |
| 242 | +*/ |
| 243 | +Camera.prototype.moveForward=function(dist){ |
| 244 | + if(dist!=0){ |
| 245 | + var diff=Camera._vec3diff(this.target,this.position); |
| 246 | + var realDist=GLMath.vec3length(diff); |
| 247 | + var factor=dist/realDist; |
| 248 | + this.position[0]+=diff[0]*factor; |
| 249 | + this.position[1]+=diff[1]*factor; |
| 250 | + this.position[2]+=diff[2]*factor; |
| 251 | + this.target[0]+=diff[0]*factor; |
| 252 | + this.target[1]+=diff[1]*factor; |
| 253 | + this.target[2]+=diff[2]*factor; |
| 254 | + this._updateLookAt(); |
| 255 | + } |
| 256 | + return this; |
| 257 | +} |
| 258 | +/** @private */ |
| 259 | +Camera.prototype._updateLookAt=function(){ |
| 260 | + this.scene.setLookAt(this.getPosition(),this.target, |
| 261 | + GLMath.quatTransform(this.angleQuat,[0,1,0])); |
| 262 | +} |
| 263 | +/** |
| 264 | + * Not documented yet. |
| 265 | +* @return {Camera} This object. |
| 266 | + */ |
| 267 | +Camera.prototype.update=function(){ |
| 268 | + var delta=this.input.deltaXY(); |
| 269 | + if(this.input.leftButton){ |
| 270 | + this.moveAngleHorizontal(0.5*delta.x) |
| 271 | + this.moveAngleVertical(0.5*delta.y) |
| 272 | + } |
| 273 | + if(this.input.keys[InputTracker.DOWN]){ |
| 274 | + this.moveForward(-0.2) |
| 275 | + } |
| 276 | + if(this.input.keys[InputTracker.UP]){ |
| 277 | + this.moveForward(0.2) |
| 278 | + } |
| 279 | + if(this.input.keys[InputTracker.LEFT]){ |
| 280 | + this.turnHorizontal(-0.5) |
| 281 | + } |
| 282 | + if(this.input.keys[InputTracker.RIGHT]){ |
| 283 | + this.turnHorizontal(0.5) |
| 284 | + } |
| 285 | + var aspect=this.scene.getAspect(); |
| 286 | + if(aspect!=this.currentAspect){ |
| 287 | + this.currentAspect=aspect; |
| 288 | + this.scene.setPerspective(this.fov,this.currentAspect,this.near,this.far); |
| 289 | + } |
| 290 | + this._updateLookAt(); |
| 291 | + return this; |
| 292 | +} |
| 293 | +function InputTracker(element){ |
| 294 | + this.leftButton=false; |
| 295 | + this.rightButton=false; |
| 296 | + this.keys={}; |
| 297 | + this.lastClient=[]; |
| 298 | + this.clientX=null; |
| 299 | + this.clientY=null; |
| 300 | + var thisObj=this; |
| 301 | + window.addEventListener("blur",function(e){ |
| 302 | + thisObj.leftButton=false; |
| 303 | + thisObj.rightButton=false; |
| 304 | + thisObj.keys={}; |
| 305 | + }) |
| 306 | + document.addEventListener("keydown",function(e){ |
| 307 | + thisObj.keys[e.keyCode]=true; |
| 308 | + }) |
| 309 | + document.addEventListener("keyup",function(e){ |
| 310 | + delete thisObj.keys[e.keyCode]; |
| 311 | + }) |
| 312 | + element.addEventListener("mousedown",function(e){ |
| 313 | + if(e.button==0){ |
| 314 | + thisObj.leftButton=true; |
| 315 | + } |
| 316 | + if(e.button==2){ |
| 317 | + thisObj.rightButton=true; |
| 318 | + } |
| 319 | + thisObj.clientX=e.clientX-InputTracker._getPageX(e.target); |
| 320 | + thisObj.clientY=e.clientY-InputTracker._getPageY(e.target); |
| 321 | + }) |
| 322 | + element.addEventListener("mouseup",function(e){ |
| 323 | + if(e.button==0){ |
| 324 | + thisObj.leftButton=false; |
| 325 | + } |
| 326 | + if(e.button==2){ |
| 327 | + thisObj.rightButton=false; |
| 328 | + } |
| 329 | + thisObj.clientX=e.clientX-InputTracker._getPageX(e.target); |
| 330 | + thisObj.clientY=e.clientY-InputTracker._getPageY(e.target); |
| 331 | + }) |
| 332 | + element.addEventListener("mousemove",function(e){ |
| 333 | + thisObj.clientX=e.clientX-InputTracker._getPageX(e.target); |
| 334 | + thisObj.clientY=e.clientY-InputTracker._getPageY(e.target); |
| 335 | + }) |
| 336 | +}; |
| 337 | +/** @private */ |
| 338 | +InputTracker._getPageX=function(o) { |
| 339 | + "use strict"; |
| 340 | + var x=0; |
| 341 | + while(o!==null && typeof o!=="undefined") { |
| 342 | + if(typeof o.offsetLeft!=="undefined") |
| 343 | + x+=o.offsetLeft; |
| 344 | + o=o.offsetParent; |
| 345 | + } |
| 346 | + return x; |
| 347 | +} |
| 348 | +/** @private */ |
| 349 | +InputTracker._getPageY=function(o) { |
| 350 | + "use strict"; |
| 351 | + var x=0; |
| 352 | + while(o!==null && typeof o!=="undefined") { |
| 353 | + if(typeof o.offsetTop!=="undefined") |
| 354 | + x+=o.offsetTop; |
| 355 | + o=o.offsetParent; |
| 356 | + } |
| 357 | + return x; |
| 358 | +} |
| 359 | +InputTracker.A=65; |
| 360 | +InputTracker.ZERO=48; |
| 361 | +InputTracker.RETURN=10; |
| 362 | +InputTracker.ENTER=13; |
| 363 | +InputTracker.TAB=9; |
| 364 | +InputTracker.SHIFT=16; |
| 365 | +InputTracker.CTRL=17; |
| 366 | +InputTracker.ESC=27; |
| 367 | +InputTracker.SPACE=32; |
| 368 | +InputTracker.PAGEUP=33; |
| 369 | +InputTracker.PAGEDOWN=34; |
| 370 | +InputTracker.END=35; |
| 371 | +InputTracker.HOME=36; |
| 372 | +InputTracker.LEFT=37; |
| 373 | +InputTracker.UP=38; |
| 374 | +InputTracker.RIGHT=39; |
| 375 | +InputTracker.DOWN=40; |
| 376 | +InputTracker.DELETE=46; |
| 377 | +InputTracker.ADD=107; |
| 378 | +InputTracker.SUBTRACT=109; |
| 379 | +/** |
| 380 | + * Not documented yet. |
| 381 | + */ |
| 382 | +InputTracker.prototype.deltaXY=function(){ |
| 383 | + var deltaX=0; |
| 384 | + var deltaY=0; |
| 385 | + if(this.clientX==null || this.clientY==null){ |
| 386 | + return {"x":0,"y":0}; |
| 387 | + } |
| 388 | + var deltaX=(this.lastClient.length==0) ? 0 : |
| 389 | + this.clientX-this.lastClient[0]; |
| 390 | + var deltaY=(this.lastClient.length==0) ? 0 : |
| 391 | + this.clientY-this.lastClient[1]; |
| 392 | + this.lastClient[0]=this.clientX; |
| 393 | + this.lastClient[1]=this.clientY; |
| 394 | + return {"x":deltaX,"y":deltaY}; |
| 395 | +} |
0 commit comments