-
Notifications
You must be signed in to change notification settings - Fork 46
/
camera.ts
299 lines (263 loc) · 9.44 KB
/
camera.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import { IPointData, Point, ObservablePoint, DEG_TO_RAD } from "@pixi/math"
import { Renderer } from "@pixi/core"
import { IDestroyOptions } from "@pixi/display"
import { Container3D } from "../container"
import { Mat4 } from "../math/mat4"
import { Ray } from "../math/ray"
import { Vec3 } from "../math/vec3"
import { Vec4 } from "../math/vec4"
import { MatrixComponent } from "../transform/matrix-component"
import { Point3D } from "../transform/point"
import { TransformId } from "../transform/transform-id"
import { Compatibility } from "../compatibility/compatibility"
import { Matrix4x4 } from "../transform/matrix"
const vec3 = new Float32Array(3)
const mat4 = new Float32Array(16)
const vec4 = new Float32Array(4)
/**
* Camera is a device from which the world is viewed.
*/
export class Camera extends Container3D implements TransformId {
private _transformId = 0
get transformId() {
return this.transform._worldID + this._transformId
}
private _projection?: MatrixComponent<Matrix4x4>
private _view?: MatrixComponent<Matrix4x4>
private _viewProjection?: MatrixComponent<Matrix4x4>
private _orthographic = false
private _orthographicSize = 10
private _obliqueness = new ObservablePoint(() => {
this._transformId++
}, undefined)
/**
* Used for making the frustum oblique, which means that one side is at a
* smaller angle to the centre line than the opposite side. Only works with
* perspective projection.
*/
get obliqueness() {
return this._obliqueness
}
set obliqueness(value: IPointData) {
this._obliqueness.copyFrom(value)
}
/** Main camera which is used by default. */
static main: Camera
/**
* Creates a new camera using the specified renderer. By default the camera
* looks towards negative z and is positioned at z = 5.
* @param renderer Renderer to use.
*/
constructor(public renderer: Renderer) {
super()
let aspect = renderer.width / renderer.height
let localID = -1
this.renderer.on("prerender", () => {
if (!this._aspect) {
// When there is no specific aspect set, this is used for the
// projection matrix to always update each frame (in case when the
// renderer aspect ratio has changed).
if (renderer.width / renderer.height !== aspect) {
this._transformId++
aspect = renderer.width / renderer.height
}
}
// @ts-ignore: _localID do exist, but be careful if this changes.
if (!this.parent && localID !== this.transform._localID) {
// When the camera is not attached to the scene hierarchy the transform
// needs to be updated manually.
this.transform.updateTransform()
// @ts-ignore: _localID do exist, but be careful if this changes.
localID = this.transform._localID
}
})
if (!Camera.main) {
Camera.main = this
}
this.transform.position.z = 5
this.transform.rotationQuaternion.setEulerAngles(0, 180, 0)
}
destroy(options?: boolean | IDestroyOptions) {
super.destroy(options)
if (this === Camera.main) {
// @ts-ignore It's ok, main camera was destroyed.
Camera.main = undefined
}
}
/**
* The camera's half-size when in orthographic mode. The visible area from
* center of the screen to the top.
*/
get orthographicSize() {
return this._orthographicSize
}
set orthographicSize(value: number) {
if (this._orthographicSize !== value) {
this._orthographicSize = value; this._transformId++
}
}
/**
* Camera will render objects uniformly, with no sense of perspective.
*/
get orthographic() {
return this._orthographic
}
set orthographic(value: boolean) {
if (this._orthographic !== value) {
this._orthographic = value; this._transformId++
}
}
/**
* Converts screen coordinates to a ray.
* @param x Screen x coordinate.
* @param y Screen y coordinate.
* @param viewSize The size of the view when not rendering to the entire screen.
*/
screenToRay(x: number, y: number, viewSize: { width: number, height: number } = this.renderer.screen) {
let screen = this.screenToWorld(x, y, 1, undefined, viewSize)
if (screen) {
if (this.orthographic) {
return new Ray(screen, this.worldTransform.forward)
}
return new Ray(this.worldTransform.position,
Point3D.subtract(screen, this.worldTransform.position))
}
}
/**
* Converts screen coordinates to world coordinates.
* @param x Screen x coordinate.
* @param y Screen y coordinate.
* @param distance Distance from the camera.
* @param point Point to set.
* @param viewSize The size of the view when not rendering to the entire screen.
*/
screenToWorld(x: number, y: number, distance: number, point = new Point3D(), viewSize: { width: number, height: number } = this.renderer.screen) {
// Make sure the transform is updated in case something has been changed,
// otherwise it may be using wrong values.
this.transform.updateTransform(this.parent?.transform)
let far = this.far
// Before doing the calculations, the far clip plane is changed to the same
// value as distance from the camera. By doing this we can just set z value
// for the clip space to 1 and the desired z position will be correct.
this.far = distance
let invertedViewProjection = Mat4.invert(this.viewProjection.array, mat4)
if (invertedViewProjection === null) {
return
}
let clipSpace = Vec4.set(
(x / viewSize.width) * 2 - 1, ((y / viewSize.height) * 2 - 1) * -1, 1, 1, vec4
)
this.far = far
let worldSpace = Vec4.transformMat4(clipSpace, invertedViewProjection, vec4)
worldSpace[3] = 1.0 / worldSpace[3]
for (let i = 0; i < 3; i++) {
worldSpace[i] *= worldSpace[3]
}
return point.set(worldSpace[0], worldSpace[1], worldSpace[2])
}
/**
* Converts world coordinates to screen coordinates.
* @param x World x coordinate.
* @param y World y coordinate.
* @param z World z coordinate.
* @param point Point to set.
* @param viewSize The size of the view when not rendering to the entire screen.
*/
worldToScreen(x: number, y: number, z: number, point = new Point(), viewSize: { width: number, height: number } = this.renderer.screen) {
// Make sure the transform is updated in case something has been changed,
// otherwise it may be using wrong values.
this.transform.updateTransform(this.parent?.transform)
let worldSpace = Vec4.set(x, y, z, 1, vec4)
let clipSpace = Vec4.transformMat4(
Vec4.transformMat4(worldSpace, this.view.array, vec4), this.projection.array, vec4
)
if (clipSpace[3] !== 0) {
for (let i = 0; i < 3; i++) {
clipSpace[i] /= clipSpace[3]
}
}
return point.set((
clipSpace[0] + 1) / 2 * viewSize.width, viewSize.height - (clipSpace[1] + 1) / 2 * viewSize.height)
}
private _fieldOfView = 60
private _near = 0.1
private _far = 1000
private _aspect?: number
/**
* The aspect ratio (width divided by height). If not set, the aspect ratio of
* the renderer will be used by default.
*/
get aspect() {
return this._aspect
}
set aspect(value: number | undefined) {
if (this._aspect !== value) {
this._aspect = value; this._transformId++
}
}
/** The vertical field of view in degrees, 60 is the default value. */
get fieldOfView() {
return this._fieldOfView
}
set fieldOfView(value: number) {
if (this._fieldOfView !== value) {
this._fieldOfView = value; this._transformId++
}
}
/** The near clipping plane distance, 0.1 is the default value. */
get near() {
return this._near
}
set near(value: number) {
if (this._near !== value) {
this._near = value; this._transformId++
}
}
/** The far clipping plane distance, 1000 is the default value. */
get far() {
return this._far
}
set far(value: number) {
if (this._far !== value) {
this._far = value; this._transformId++
}
}
/** Returns the projection matrix. */
get projection() {
if (!this._projection) {
this._projection = new MatrixComponent<Matrix4x4>(this, new Matrix4x4(), data => {
const aspect = this._aspect || this.renderer.width / this.renderer.height
if (this._orthographic) {
Mat4.ortho(-this._orthographicSize * aspect, this._orthographicSize * aspect, -this._orthographicSize, this._orthographicSize, this._near, this._far, data.array)
} else {
Mat4.perspective(this._fieldOfView * DEG_TO_RAD, aspect, this._near, this._far, data.array)
data.array[8] = this._obliqueness.x
data.array[9] = this._obliqueness.y
}
})
}
return this._projection.data
}
/** Returns the view matrix. */
get view() {
if (!this._view) {
this._view = new MatrixComponent<Matrix4x4>(this, new Matrix4x4(), data => {
const target = Vec3.add(
this.worldTransform.position.array, this.worldTransform.forward.array, vec3)
Mat4.lookAt(this.worldTransform.position.array,
target, this.worldTransform.up.array, data.array)
})
}
return this._view.data
}
/** Returns the view projection matrix. */
get viewProjection() {
if (!this._viewProjection) {
this._viewProjection = new MatrixComponent<Matrix4x4>(this, new Matrix4x4(), data => {
Mat4.multiply(this.projection.array, this.view.array, data.array)
})
}
return this._viewProjection.data
}
}
Compatibility.installRendererPlugin("camera", Camera)