@d5techs/3dgs-lib 1.4.25 → 1.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/3dgs-lib.cjs +205 -32
- package/dist/3dgs-lib.cjs.map +1 -1
- package/dist/3dgs-lib.js +205 -32
- package/dist/3dgs-lib.js.map +1 -1
- package/dist/App.d.ts +4 -0
- package/dist/core/OrbitControls.d.ts +1 -0
- package/dist/gs/SOGEncoder.d.ts +1 -1
- package/package.json +1 -1
package/dist/3dgs-lib.cjs
CHANGED
|
@@ -653,6 +653,8 @@ const _OrbitControls = class _OrbitControls {
|
|
|
653
653
|
__publicField(this, "lastTouchCenter", { x: 0, y: 0 });
|
|
654
654
|
// 启用/禁用
|
|
655
655
|
__publicField(this, "enabled", true);
|
|
656
|
+
// 双击拾取回调:返回世界坐标则聚焦,返回 null 则忽略
|
|
657
|
+
__publicField(this, "pickWorldPosition", null);
|
|
656
658
|
// 绑定的事件处理函数(用于移除监听器)
|
|
657
659
|
__publicField(this, "boundOnMouseDown");
|
|
658
660
|
__publicField(this, "boundOnMouseMove");
|
|
@@ -839,6 +841,13 @@ const _OrbitControls = class _OrbitControls {
|
|
|
839
841
|
}
|
|
840
842
|
onDblClick(e) {
|
|
841
843
|
if (!this.enabled) return;
|
|
844
|
+
if (this.pickWorldPosition) {
|
|
845
|
+
const hit = this.pickWorldPosition(e.clientX, e.clientY);
|
|
846
|
+
if (hit) {
|
|
847
|
+
this.animateToTarget(hit);
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
842
851
|
const rect = this.canvas.getBoundingClientRect();
|
|
843
852
|
const ndcX = (e.clientX - rect.left) / rect.width * 2 - 1;
|
|
844
853
|
const ndcY = -((e.clientY - rect.top) / rect.height * 2 - 1);
|
|
@@ -6214,7 +6223,10 @@ function buildCodebook(values, k = 256) {
|
|
|
6214
6223
|
for (let i = 0; i < N; i++) {
|
|
6215
6224
|
const uniformPos = (sorted[i] - vMin) / vRange;
|
|
6216
6225
|
const quantilePos = i / N;
|
|
6217
|
-
const bin = Math.min(
|
|
6226
|
+
const bin = Math.min(
|
|
6227
|
+
H - 1,
|
|
6228
|
+
Math.floor(H * (beta * quantilePos + (1 - beta) * uniformPos))
|
|
6229
|
+
);
|
|
6218
6230
|
counts[bin]++;
|
|
6219
6231
|
sums[bin] += sorted[i];
|
|
6220
6232
|
}
|
|
@@ -6291,7 +6303,8 @@ function buildCodebook(values, k = 256) {
|
|
|
6291
6303
|
centroidValues.sort();
|
|
6292
6304
|
const result = new Array(k);
|
|
6293
6305
|
for (let i = 0; i < effectiveK; i++) result[i] = centroidValues[i];
|
|
6294
|
-
for (let i = effectiveK; i < k; i++)
|
|
6306
|
+
for (let i = effectiveK; i < k; i++)
|
|
6307
|
+
result[i] = centroidValues[effectiveK - 1];
|
|
6295
6308
|
return result;
|
|
6296
6309
|
}
|
|
6297
6310
|
function findNearest(sortedCB, value) {
|
|
@@ -6419,27 +6432,66 @@ async function gpuAssignLabels(device, pointsFlat, centroidsFlat, numPoints, num
|
|
|
6419
6432
|
const WORKGROUP_SIZE2 = 64;
|
|
6420
6433
|
const BATCH_SIZE = 1024 * WORKGROUP_SIZE2;
|
|
6421
6434
|
const numBatches = Math.ceil(numPoints / BATCH_SIZE);
|
|
6422
|
-
const shaderModule = device.createShaderModule({
|
|
6435
|
+
const shaderModule = device.createShaderModule({
|
|
6436
|
+
code: getGpuClusterShader(dim)
|
|
6437
|
+
});
|
|
6423
6438
|
const bindGroupLayout = device.createBindGroupLayout({
|
|
6424
6439
|
entries: [
|
|
6425
|
-
{
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6440
|
+
{
|
|
6441
|
+
binding: 0,
|
|
6442
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
6443
|
+
buffer: { type: "uniform" }
|
|
6444
|
+
},
|
|
6445
|
+
{
|
|
6446
|
+
binding: 1,
|
|
6447
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
6448
|
+
buffer: { type: "read-only-storage" }
|
|
6449
|
+
},
|
|
6450
|
+
{
|
|
6451
|
+
binding: 2,
|
|
6452
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
6453
|
+
buffer: { type: "read-only-storage" }
|
|
6454
|
+
},
|
|
6455
|
+
{
|
|
6456
|
+
binding: 3,
|
|
6457
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
6458
|
+
buffer: { type: "storage" }
|
|
6459
|
+
}
|
|
6429
6460
|
]
|
|
6430
6461
|
});
|
|
6431
6462
|
const pipeline = device.createComputePipeline({
|
|
6432
|
-
layout: device.createPipelineLayout({
|
|
6463
|
+
layout: device.createPipelineLayout({
|
|
6464
|
+
bindGroupLayouts: [bindGroupLayout]
|
|
6465
|
+
}),
|
|
6433
6466
|
compute: { module: shaderModule, entryPoint: "main" }
|
|
6434
6467
|
});
|
|
6435
|
-
const uniformBuf = device.createBuffer({
|
|
6436
|
-
|
|
6437
|
-
|
|
6468
|
+
const uniformBuf = device.createBuffer({
|
|
6469
|
+
size: 8,
|
|
6470
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
6471
|
+
});
|
|
6472
|
+
const centroidsBuf = device.createBuffer({
|
|
6473
|
+
size: centroidsFlat.byteLength,
|
|
6474
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
6475
|
+
});
|
|
6476
|
+
device.queue.writeBuffer(
|
|
6477
|
+
centroidsBuf,
|
|
6478
|
+
0,
|
|
6479
|
+
centroidsFlat.buffer
|
|
6480
|
+
);
|
|
6438
6481
|
const pointsBufSize = BATCH_SIZE * dim * 4;
|
|
6439
|
-
const pointsBuf = device.createBuffer({
|
|
6482
|
+
const pointsBuf = device.createBuffer({
|
|
6483
|
+
size: pointsBufSize,
|
|
6484
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
6485
|
+
});
|
|
6440
6486
|
const resultsBufSize = BATCH_SIZE * 4;
|
|
6441
|
-
const resultsBuf = device.createBuffer({
|
|
6442
|
-
|
|
6487
|
+
const resultsBuf = device.createBuffer({
|
|
6488
|
+
size: resultsBufSize,
|
|
6489
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
6490
|
+
});
|
|
6491
|
+
const readbackBuf = device.createBuffer({
|
|
6492
|
+
size: resultsBufSize,
|
|
6493
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
6494
|
+
});
|
|
6443
6495
|
const labels = new Uint32Array(numPoints);
|
|
6444
6496
|
const bindGroup = device.createBindGroup({
|
|
6445
6497
|
layout: bindGroupLayout,
|
|
@@ -6454,18 +6506,36 @@ async function gpuAssignLabels(device, pointsFlat, centroidsFlat, numPoints, num
|
|
|
6454
6506
|
const offset = batch * BATCH_SIZE;
|
|
6455
6507
|
const currentBatchSize = Math.min(numPoints - offset, BATCH_SIZE);
|
|
6456
6508
|
const groups = Math.ceil(currentBatchSize / WORKGROUP_SIZE2);
|
|
6457
|
-
device.queue.writeBuffer(
|
|
6458
|
-
|
|
6509
|
+
device.queue.writeBuffer(
|
|
6510
|
+
uniformBuf,
|
|
6511
|
+
0,
|
|
6512
|
+
new Uint32Array([currentBatchSize, numCentroids])
|
|
6513
|
+
);
|
|
6514
|
+
device.queue.writeBuffer(
|
|
6515
|
+
pointsBuf,
|
|
6516
|
+
0,
|
|
6517
|
+
pointsFlat.buffer,
|
|
6518
|
+
offset * dim * 4,
|
|
6519
|
+
currentBatchSize * dim * 4
|
|
6520
|
+
);
|
|
6459
6521
|
const encoder = device.createCommandEncoder();
|
|
6460
6522
|
const pass = encoder.beginComputePass();
|
|
6461
6523
|
pass.setPipeline(pipeline);
|
|
6462
6524
|
pass.setBindGroup(0, bindGroup);
|
|
6463
6525
|
pass.dispatchWorkgroups(groups);
|
|
6464
6526
|
pass.end();
|
|
6465
|
-
encoder.copyBufferToBuffer(
|
|
6527
|
+
encoder.copyBufferToBuffer(
|
|
6528
|
+
resultsBuf,
|
|
6529
|
+
0,
|
|
6530
|
+
readbackBuf,
|
|
6531
|
+
0,
|
|
6532
|
+
currentBatchSize * 4
|
|
6533
|
+
);
|
|
6466
6534
|
device.queue.submit([encoder.finish()]);
|
|
6467
6535
|
await readbackBuf.mapAsync(GPUMapMode.READ);
|
|
6468
|
-
const mapped = new Uint32Array(
|
|
6536
|
+
const mapped = new Uint32Array(
|
|
6537
|
+
readbackBuf.getMappedRange(0, currentBatchSize * 4)
|
|
6538
|
+
);
|
|
6469
6539
|
labels.set(mapped, offset);
|
|
6470
6540
|
readbackBuf.unmap();
|
|
6471
6541
|
}
|
|
@@ -6489,13 +6559,23 @@ async function kmeansGpu(pointsFlat, n, dim, k, iterations, onProgress) {
|
|
|
6489
6559
|
const centroids = new Float32Array(k * dim);
|
|
6490
6560
|
const indices = pickRandomIndices(n, k);
|
|
6491
6561
|
for (let i = 0; i < k; i++) {
|
|
6492
|
-
centroids.set(
|
|
6562
|
+
centroids.set(
|
|
6563
|
+
pointsFlat.subarray(indices[i] * dim, indices[i] * dim + dim),
|
|
6564
|
+
i * dim
|
|
6565
|
+
);
|
|
6493
6566
|
}
|
|
6494
6567
|
const device = await getGpuDevice();
|
|
6495
6568
|
for (let iter = 0; iter < iterations; iter++) {
|
|
6496
6569
|
onProgress == null ? void 0 : onProgress(iter, iterations);
|
|
6497
6570
|
if (device) {
|
|
6498
|
-
const newLabels = await gpuAssignLabels(
|
|
6571
|
+
const newLabels = await gpuAssignLabels(
|
|
6572
|
+
device,
|
|
6573
|
+
pointsFlat,
|
|
6574
|
+
centroids,
|
|
6575
|
+
n,
|
|
6576
|
+
k,
|
|
6577
|
+
dim
|
|
6578
|
+
);
|
|
6499
6579
|
labels.set(newLabels);
|
|
6500
6580
|
} else {
|
|
6501
6581
|
assignLabelsCpu(pointsFlat, centroids, labels, n, k, dim);
|
|
@@ -6504,7 +6584,14 @@ async function kmeansGpu(pointsFlat, n, dim, k, iterations, onProgress) {
|
|
|
6504
6584
|
}
|
|
6505
6585
|
onProgress == null ? void 0 : onProgress(iterations, iterations);
|
|
6506
6586
|
if (device) {
|
|
6507
|
-
const finalLabels = await gpuAssignLabels(
|
|
6587
|
+
const finalLabels = await gpuAssignLabels(
|
|
6588
|
+
device,
|
|
6589
|
+
pointsFlat,
|
|
6590
|
+
centroids,
|
|
6591
|
+
n,
|
|
6592
|
+
k,
|
|
6593
|
+
dim
|
|
6594
|
+
);
|
|
6508
6595
|
labels.set(finalLabels);
|
|
6509
6596
|
} else {
|
|
6510
6597
|
assignLabelsCpu(pointsFlat, centroids, labels, n, k, dim);
|
|
@@ -6550,7 +6637,8 @@ function updateCentroids(points, centroids, labels, n, k, dim) {
|
|
|
6550
6637
|
for (let c = 0; c < k; c++) {
|
|
6551
6638
|
if (counts[c] > 0) {
|
|
6552
6639
|
const off = c * dim;
|
|
6553
|
-
for (let d = 0; d < dim; d++)
|
|
6640
|
+
for (let d = 0; d < dim; d++)
|
|
6641
|
+
centroids[off + d] = sums[off + d] / counts[c];
|
|
6554
6642
|
} else {
|
|
6555
6643
|
const idx = Math.floor(Math.random() * n);
|
|
6556
6644
|
centroids.set(points.subarray(idx * dim, idx * dim + dim), c * dim);
|
|
@@ -6583,9 +6671,18 @@ function mortonSort(positions, count) {
|
|
|
6583
6671
|
const morton = new Uint32Array(count);
|
|
6584
6672
|
const indices = new Uint32Array(count);
|
|
6585
6673
|
for (let i = 0; i < count; i++) {
|
|
6586
|
-
const ix = Math.min(
|
|
6587
|
-
|
|
6588
|
-
|
|
6674
|
+
const ix = Math.min(
|
|
6675
|
+
1023,
|
|
6676
|
+
Math.floor(1024 * (positions[i * 3] - minx) / xlen)
|
|
6677
|
+
);
|
|
6678
|
+
const iy = Math.min(
|
|
6679
|
+
1023,
|
|
6680
|
+
Math.floor(1024 * (positions[i * 3 + 1] - miny) / ylen)
|
|
6681
|
+
);
|
|
6682
|
+
const iz = Math.min(
|
|
6683
|
+
1023,
|
|
6684
|
+
Math.floor(1024 * (positions[i * 3 + 2] - minz) / zlen)
|
|
6685
|
+
);
|
|
6589
6686
|
morton[i] = (part1By2(iz) << 2) + (part1By2(iy) << 1) + part1By2(ix);
|
|
6590
6687
|
indices[i] = i;
|
|
6591
6688
|
}
|
|
@@ -6722,7 +6819,8 @@ async function serializeSOG(data, coordinateSystem = "blender", options = {}, on
|
|
|
6722
6819
|
onProgress == null ? void 0 : onProgress("encode_scale", 0.2);
|
|
6723
6820
|
await yieldToUI();
|
|
6724
6821
|
const logScales = new Float32Array(count * 3);
|
|
6725
|
-
for (let i = 0; i < count * 3; i++)
|
|
6822
|
+
for (let i = 0; i < count * 3; i++)
|
|
6823
|
+
logScales[i] = Math.log(Math.max(1e-8, sSca[i]));
|
|
6726
6824
|
const scaleCodebook = buildCodebook(logScales, 256);
|
|
6727
6825
|
const scalesData = new Uint8ClampedArray(totalPixels * 4);
|
|
6728
6826
|
scalesData.fill(255);
|
|
@@ -6782,9 +6880,15 @@ async function serializeSOG(data, coordinateSystem = "blender", options = {}, on
|
|
|
6782
6880
|
r1 = qx;
|
|
6783
6881
|
r2 = qy;
|
|
6784
6882
|
}
|
|
6785
|
-
quatsData[off] = Math.round(
|
|
6786
|
-
|
|
6787
|
-
|
|
6883
|
+
quatsData[off] = Math.round(
|
|
6884
|
+
Math.max(0, Math.min(255, (r0 / SQRT2 + 0.5) * 255))
|
|
6885
|
+
);
|
|
6886
|
+
quatsData[off + 1] = Math.round(
|
|
6887
|
+
Math.max(0, Math.min(255, (r1 / SQRT2 + 0.5) * 255))
|
|
6888
|
+
);
|
|
6889
|
+
quatsData[off + 2] = Math.round(
|
|
6890
|
+
Math.max(0, Math.min(255, (r2 / SQRT2 + 0.5) * 255))
|
|
6891
|
+
);
|
|
6788
6892
|
quatsData[off + 3] = largest + 252;
|
|
6789
6893
|
}
|
|
6790
6894
|
onProgress == null ? void 0 : onProgress("encode_color", 0.4);
|
|
@@ -6840,7 +6944,8 @@ async function serializeSOG(data, coordinateSystem = "blender", options = {}, on
|
|
|
6840
6944
|
await yieldToUI();
|
|
6841
6945
|
const allCoeffValues = new Float32Array(effectivePaletteSize * shDim);
|
|
6842
6946
|
for (let c = 0; c < effectivePaletteSize; c++) {
|
|
6843
|
-
for (let d = 0; d < shDim; d++)
|
|
6947
|
+
for (let d = 0; d < shDim; d++)
|
|
6948
|
+
allCoeffValues[c * shDim + d] = centroids[c * shDim + d];
|
|
6844
6949
|
}
|
|
6845
6950
|
const shCodebook = buildCodebook(allCoeffValues, 256);
|
|
6846
6951
|
const centW = 64 * numCoeffs;
|
|
@@ -6854,8 +6959,14 @@ async function serializeSOG(data, coordinateSystem = "blender", options = {}, on
|
|
|
6854
6959
|
const off = (v * centW + u) * 4;
|
|
6855
6960
|
const cOff = n * shDim;
|
|
6856
6961
|
centData[off] = findNearest(shCodebook, centroids[cOff + c * 3]);
|
|
6857
|
-
centData[off + 1] = findNearest(
|
|
6858
|
-
|
|
6962
|
+
centData[off + 1] = findNearest(
|
|
6963
|
+
shCodebook,
|
|
6964
|
+
centroids[cOff + c * 3 + 1]
|
|
6965
|
+
);
|
|
6966
|
+
centData[off + 2] = findNearest(
|
|
6967
|
+
shCodebook,
|
|
6968
|
+
centroids[cOff + c * 3 + 2]
|
|
6969
|
+
);
|
|
6859
6970
|
centData[off + 3] = 255;
|
|
6860
6971
|
}
|
|
6861
6972
|
}
|
|
@@ -18113,6 +18224,7 @@ class App {
|
|
|
18113
18224
|
this.camera = new Camera();
|
|
18114
18225
|
this.camera.setAspect(this.renderer.getAspectRatio());
|
|
18115
18226
|
this.controls = new OrbitControls(this.camera, this.canvas);
|
|
18227
|
+
this.controls.pickWorldPosition = (cx, cy) => this.pickSplatPosition(cx, cy);
|
|
18116
18228
|
this.meshRenderer = new MeshRenderer(this.renderer, this.camera);
|
|
18117
18229
|
this.glbLoader = new GLBLoader(this.renderer.device);
|
|
18118
18230
|
this.objLoader = new OBJLoader(this.renderer.device);
|
|
@@ -18733,6 +18845,67 @@ class App {
|
|
|
18733
18845
|
getRenderer() {
|
|
18734
18846
|
return this.renderer;
|
|
18735
18847
|
}
|
|
18848
|
+
/**
|
|
18849
|
+
* CPU splat 拾取:在屏幕坐标 (clientX, clientY) 处找最近的 splat,返回世界坐标或 null
|
|
18850
|
+
*/
|
|
18851
|
+
pickSplatPosition(clientX, clientY) {
|
|
18852
|
+
const gsRenderer = this.sceneManager.getGSRenderer();
|
|
18853
|
+
if (!gsRenderer) return null;
|
|
18854
|
+
const positions = gsRenderer.getCPUPositions();
|
|
18855
|
+
if (!positions) return null;
|
|
18856
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
18857
|
+
const ndcX = (clientX - rect.left) / rect.width * 2 - 1;
|
|
18858
|
+
const ndcY = -((clientY - rect.top) / rect.height * 2 - 1);
|
|
18859
|
+
const modelMatrix = gsRenderer.getModelMatrix();
|
|
18860
|
+
const vp = this.camera.viewProjectionMatrix;
|
|
18861
|
+
const count = positions.length / 3;
|
|
18862
|
+
const step = count > 3e5 ? Math.ceil(count / 3e5) : 1;
|
|
18863
|
+
const halfW = rect.width / 2;
|
|
18864
|
+
const halfH = rect.height / 2;
|
|
18865
|
+
const pickRadiusPx = 20;
|
|
18866
|
+
const pickRadSq = pickRadiusPx * pickRadiusPx;
|
|
18867
|
+
let bestCamDistSq = Infinity;
|
|
18868
|
+
let bestIdx = -1;
|
|
18869
|
+
let bestScreenDistSq = Infinity;
|
|
18870
|
+
const m0 = modelMatrix[0], m1 = modelMatrix[1], m2 = modelMatrix[2];
|
|
18871
|
+
const m4 = modelMatrix[4], m5 = modelMatrix[5], m6 = modelMatrix[6];
|
|
18872
|
+
const m8 = modelMatrix[8], m9 = modelMatrix[9], m10 = modelMatrix[10];
|
|
18873
|
+
const m12 = modelMatrix[12], m13 = modelMatrix[13], m14 = modelMatrix[14];
|
|
18874
|
+
const v0 = vp[0], v1 = vp[1], v3 = vp[3];
|
|
18875
|
+
const v4 = vp[4], v5 = vp[5], v7 = vp[7];
|
|
18876
|
+
const v8 = vp[8], v9 = vp[9], v11 = vp[11];
|
|
18877
|
+
const v12 = vp[12], v13 = vp[13], v15 = vp[15];
|
|
18878
|
+
const rox = this.camera.position[0], roy = this.camera.position[1], roz = this.camera.position[2];
|
|
18879
|
+
for (let i = 0; i < count; i += step) {
|
|
18880
|
+
const i3 = i * 3;
|
|
18881
|
+
const lx = positions[i3], ly = positions[i3 + 1], lz = positions[i3 + 2];
|
|
18882
|
+
const tx = m0 * lx + m4 * ly + m8 * lz + m12;
|
|
18883
|
+
const ty = m1 * lx + m5 * ly + m9 * lz + m13;
|
|
18884
|
+
const tz = m2 * lx + m6 * ly + m10 * lz + m14;
|
|
18885
|
+
const cw = v3 * tx + v7 * ty + v11 * tz + v15;
|
|
18886
|
+
if (cw <= 0) continue;
|
|
18887
|
+
const invCw = 1 / cw;
|
|
18888
|
+
const pixX = ((v0 * tx + v4 * ty + v8 * tz + v12) * invCw - ndcX) * halfW;
|
|
18889
|
+
const pixY = ((v1 * tx + v5 * ty + v9 * tz + v13) * invCw - ndcY) * halfH;
|
|
18890
|
+
const screenDistSq = pixX * pixX + pixY * pixY;
|
|
18891
|
+
if (screenDistSq < pickRadSq) {
|
|
18892
|
+
const dx = tx - rox, dy = ty - roy, dz = tz - roz;
|
|
18893
|
+
const camDistSq = dx * dx + dy * dy + dz * dz;
|
|
18894
|
+
if (camDistSq < bestCamDistSq * 0.98 || camDistSq < bestCamDistSq * 1.02 && screenDistSq < bestScreenDistSq) {
|
|
18895
|
+
bestCamDistSq = camDistSq;
|
|
18896
|
+
bestScreenDistSq = screenDistSq;
|
|
18897
|
+
bestIdx = i;
|
|
18898
|
+
}
|
|
18899
|
+
}
|
|
18900
|
+
}
|
|
18901
|
+
if (bestIdx < 0) return null;
|
|
18902
|
+
const hx = positions[bestIdx * 3], hy = positions[bestIdx * 3 + 1], hz = positions[bestIdx * 3 + 2];
|
|
18903
|
+
return [
|
|
18904
|
+
m0 * hx + m4 * hy + m8 * hz + m12,
|
|
18905
|
+
m1 * hx + m5 * hy + m9 * hz + m13,
|
|
18906
|
+
m2 * hx + m6 * hy + m10 * hz + m14
|
|
18907
|
+
];
|
|
18908
|
+
}
|
|
18736
18909
|
getCamera() {
|
|
18737
18910
|
return this.camera;
|
|
18738
18911
|
}
|