fix(simulation): standardize streamed rendering and UGV proxies
This commit is contained in:
@@ -118,6 +118,9 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
private renderIntervalMs = 1000 / 60;
|
private renderIntervalMs = 1000 / 60;
|
||||||
private renderAccumulatorMs = 0;
|
private renderAccumulatorMs = 0;
|
||||||
private disposed = false;
|
private disposed = false;
|
||||||
|
private requestGsplatFrame = (): void => {
|
||||||
|
this.requestRender();
|
||||||
|
};
|
||||||
private updateFrame = (deltaSeconds: number): void => {
|
private updateFrame = (deltaSeconds: number): void => {
|
||||||
if (!this.app || !this.camera || this.disposed) return;
|
if (!this.app || !this.camera || this.disposed) return;
|
||||||
this.ugvController?.update(deltaSeconds);
|
this.ugvController?.update(deltaSeconds);
|
||||||
@@ -158,6 +161,7 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
this.app = app;
|
this.app = app;
|
||||||
app.autoRender = false;
|
app.autoRender = false;
|
||||||
app.on("update", this.updateFrame);
|
app.on("update", this.updateFrame);
|
||||||
|
app.systems.gsplat?.on("frame:request", this.requestGsplatFrame);
|
||||||
app.setCanvasFillMode(FILLMODE_NONE, 1, 1);
|
app.setCanvasFillMode(FILLMODE_NONE, 1, 1);
|
||||||
app.setCanvasResolution(RESOLUTION_AUTO);
|
app.setCanvasResolution(RESOLUTION_AUTO);
|
||||||
app.scene.ambientLight = new Color(0.35, 0.37, 0.42);
|
app.scene.ambientLight = new Color(0.35, 0.37, 0.42);
|
||||||
@@ -379,6 +383,7 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
if (this.app) {
|
if (this.app) {
|
||||||
this.app.autoRender = false;
|
this.app.autoRender = false;
|
||||||
this.app.renderNextFrame = false;
|
this.app.renderNextFrame = false;
|
||||||
|
this.app.systems.gsplat?.off("frame:request", this.requestGsplatFrame);
|
||||||
this.app.off("update", this.updateFrame);
|
this.app.off("update", this.updateFrame);
|
||||||
this.app.destroy();
|
this.app.destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,7 +232,6 @@ export class SimulationUgvController {
|
|||||||
private active = false;
|
private active = false;
|
||||||
private vehicleEntity: Entity | null = null;
|
private vehicleEntity: Entity | null = null;
|
||||||
private vehicle: NativeRaycastVehicle | null = null;
|
private vehicle: NativeRaycastVehicle | null = null;
|
||||||
private vehicleTuning: NativeObject | null = null;
|
|
||||||
private vehicleRaycaster: NativeObject | null = null;
|
private vehicleRaycaster: NativeObject | null = null;
|
||||||
private tyreImpulseNative: NativeVector3 | null = null;
|
private tyreImpulseNative: NativeVector3 | null = null;
|
||||||
private tyreRelativePositionNative: NativeVector3 | null = null;
|
private tyreRelativePositionNative: NativeVector3 | null = null;
|
||||||
@@ -599,7 +598,12 @@ export class SimulationUgvController {
|
|||||||
this.app,
|
this.app,
|
||||||
meshInstance.mesh,
|
meshInstance.mesh,
|
||||||
new Mat4().mul2(rootInverse, meshInstance.node.getWorldTransform()),
|
new Mat4().mul2(rootInverse, meshInstance.node.getWorldTransform()),
|
||||||
targetRatio,
|
Math.max(
|
||||||
|
1,
|
||||||
|
Math.floor(
|
||||||
|
Math.floor((meshInstance.mesh.primitive[0]?.count ?? 0) / 3) * targetRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (!physicsMesh) continue;
|
if (!physicsMesh) continue;
|
||||||
const entity = new Entity(`UGV physics proxy ${index + 1}`);
|
const entity = new Entity(`UGV physics proxy ${index + 1}`);
|
||||||
@@ -745,7 +749,6 @@ export class SimulationUgvController {
|
|||||||
|
|
||||||
dynamicsWorld.addAction(nativeVehicle);
|
dynamicsWorld.addAction(nativeVehicle);
|
||||||
this.vehicleEntity = vehicle;
|
this.vehicleEntity = vehicle;
|
||||||
this.vehicleTuning = tuning;
|
|
||||||
this.vehicleRaycaster = raycaster;
|
this.vehicleRaycaster = raycaster;
|
||||||
this.vehicle = nativeVehicle;
|
this.vehicle = nativeVehicle;
|
||||||
this.dynamicsWorld = dynamicsWorld;
|
this.dynamicsWorld = dynamicsWorld;
|
||||||
@@ -861,12 +864,10 @@ export class SimulationUgvController {
|
|||||||
}
|
}
|
||||||
if (this.vehicle) runCleanup("destroy vehicle", () => this.ammo.destroy(this.vehicle as NativeObject));
|
if (this.vehicle) runCleanup("destroy vehicle", () => this.ammo.destroy(this.vehicle as NativeObject));
|
||||||
if (this.vehicleRaycaster) runCleanup("destroy vehicle raycaster", () => this.ammo.destroy(this.vehicleRaycaster as NativeObject));
|
if (this.vehicleRaycaster) runCleanup("destroy vehicle raycaster", () => this.ammo.destroy(this.vehicleRaycaster as NativeObject));
|
||||||
if (this.vehicleTuning) runCleanup("destroy vehicle tuning", () => this.ammo.destroy(this.vehicleTuning as NativeObject));
|
|
||||||
if (this.tyreImpulseNative) runCleanup("destroy tyre impulse vector", () => this.ammo.destroy(this.tyreImpulseNative as NativeObject));
|
if (this.tyreImpulseNative) runCleanup("destroy tyre impulse vector", () => this.ammo.destroy(this.tyreImpulseNative as NativeObject));
|
||||||
if (this.tyreRelativePositionNative) runCleanup("destroy tyre relative-position vector", () => this.ammo.destroy(this.tyreRelativePositionNative as NativeObject));
|
if (this.tyreRelativePositionNative) runCleanup("destroy tyre relative-position vector", () => this.ammo.destroy(this.tyreRelativePositionNative as NativeObject));
|
||||||
this.vehicle = null;
|
this.vehicle = null;
|
||||||
this.vehicleRaycaster = null;
|
this.vehicleRaycaster = null;
|
||||||
this.vehicleTuning = null;
|
|
||||||
this.tyreImpulseNative = null;
|
this.tyreImpulseNative = null;
|
||||||
this.tyreRelativePositionNative = null;
|
this.tyreRelativePositionNative = null;
|
||||||
this.dynamicsWorld = null;
|
this.dynamicsWorld = null;
|
||||||
@@ -1005,7 +1006,7 @@ function createPhysicsProxyMesh(
|
|||||||
app: Application,
|
app: Application,
|
||||||
source: Mesh,
|
source: Mesh,
|
||||||
localTransform: Mat4,
|
localTransform: Mat4,
|
||||||
targetRatio: number,
|
targetTriangleCount: number,
|
||||||
): Mesh | null {
|
): Mesh | null {
|
||||||
const primitive = source.primitive[0];
|
const primitive = source.primitive[0];
|
||||||
const sourceVertexCount = source.vertexBuffer?.numVertices ?? 0;
|
const sourceVertexCount = source.vertexBuffer?.numVertices ?? 0;
|
||||||
@@ -1039,8 +1040,11 @@ function createPhysicsProxyMesh(
|
|||||||
if (triangleIndexCount < 3) return null;
|
if (triangleIndexCount < 3) return null;
|
||||||
if (triangleIndexCount !== indices.length) indices = indices.slice(0, triangleIndexCount);
|
if (triangleIndexCount !== indices.length) indices = indices.slice(0, triangleIndexCount);
|
||||||
|
|
||||||
if (targetRatio < 1) {
|
const targetIndexCount = Math.max(
|
||||||
const targetIndexCount = Math.max(3, Math.floor(indices.length * targetRatio / 3) * 3);
|
3,
|
||||||
|
Math.min(indices.length, Math.floor(targetTriangleCount) * 3),
|
||||||
|
);
|
||||||
|
if (indices.length > targetIndexCount) {
|
||||||
const [simplifiedIndices] = MeshoptSimplifier.simplify(
|
const [simplifiedIndices] = MeshoptSimplifier.simplify(
|
||||||
indices,
|
indices,
|
||||||
transformed,
|
transformed,
|
||||||
@@ -1049,6 +1053,26 @@ function createPhysicsProxyMesh(
|
|||||||
PHYSICS_PROXY_ERROR,
|
PHYSICS_PROXY_ERROR,
|
||||||
);
|
);
|
||||||
indices = new Uint32Array(simplifiedIndices);
|
indices = new Uint32Array(simplifiedIndices);
|
||||||
|
if (indices.length > targetIndexCount) {
|
||||||
|
// Topologically noisy scanner meshes can make the quality simplifier stop
|
||||||
|
// above its requested target. Ammo must never receive that unbounded result:
|
||||||
|
// the spatial fallback preserves the surface envelope while enforcing the
|
||||||
|
// same deterministic physics budget for every imported location.
|
||||||
|
const [boundedIndices] = MeshoptSimplifier.simplifySloppy(
|
||||||
|
indices,
|
||||||
|
transformed,
|
||||||
|
3,
|
||||||
|
null,
|
||||||
|
targetIndexCount,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
indices = new Uint32Array(boundedIndices);
|
||||||
|
}
|
||||||
|
if (indices.length > targetIndexCount) {
|
||||||
|
throw new Error(
|
||||||
|
`Physics proxy превысил лимит: ${Math.floor(indices.length / 3)} > ${Math.floor(targetIndexCount / 3)} треугольников.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const compactedIndices = new Uint32Array(indices);
|
const compactedIndices = new Uint32Array(indices);
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
|
|||||||
assert.match(runtime, /maximum: \{[^}]*lodRangeMin: 0, lodRangeMax: 5, pixelRatio: 2, splatBudget: 4_000_000, targetFps: 60/);
|
assert.match(runtime, /maximum: \{[^}]*lodRangeMin: 0, lodRangeMax: 5, pixelRatio: 2, splatBudget: 4_000_000, targetFps: 60/);
|
||||||
assert.match(runtime, /app\.scene\.gsplat\.splatBudget = profile\.splatBudget/);
|
assert.match(runtime, /app\.scene\.gsplat\.splatBudget = profile\.splatBudget/);
|
||||||
assert.match(runtime, /app\.autoRender = false/);
|
assert.match(runtime, /app\.autoRender = false/);
|
||||||
|
assert.match(runtime, /app\.systems\.gsplat\?\.on\("frame:request", this\.requestGsplatFrame\)/);
|
||||||
|
assert.match(runtime, /app\.systems\.gsplat\?\.off\("frame:request", this\.requestGsplatFrame\)/);
|
||||||
assert.match(runtime, /app\.on\("update", this\.updateFrame\)/);
|
assert.match(runtime, /app\.on\("update", this\.updateFrame\)/);
|
||||||
assert.match(runtime, /if \(!cameraChanged && this\.controlMode === "free"\) return/);
|
assert.match(runtime, /if \(!cameraChanged && this\.controlMode === "free"\) return/);
|
||||||
assert.doesNotMatch(runtime, /app\.on\("frameupdate"/);
|
assert.doesNotMatch(runtime, /app\.on\("frameupdate"/);
|
||||||
@@ -162,6 +164,8 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
|
|||||||
assert.match(ugv, /new this\.ammo\.btRaycastVehicle/);
|
assert.match(ugv, /new this\.ammo\.btRaycastVehicle/);
|
||||||
assert.match(ugv, /type: "mesh"/);
|
assert.match(ugv, /type: "mesh"/);
|
||||||
assert.match(ugv, /MeshoptSimplifier\.simplify/);
|
assert.match(ugv, /MeshoptSimplifier\.simplify/);
|
||||||
|
assert.match(ugv, /MeshoptSimplifier\.simplifySloppy/);
|
||||||
|
assert.match(ugv, /Physics proxy превысил лимит/);
|
||||||
assert.match(ugv, /PHYSICS_PROXY_MAX_TRIANGLES = 240_000/);
|
assert.match(ugv, /PHYSICS_PROXY_MAX_TRIANGLES = 240_000/);
|
||||||
assert.match(ugv, /createPhysicsProxyMesh/);
|
assert.match(ugv, /createPhysicsProxyMesh/);
|
||||||
assert.match(ugv, /findSafeSpawnPosition/);
|
assert.match(ugv, /findSafeSpawnPosition/);
|
||||||
|
|||||||
Reference in New Issue
Block a user