Advance M8-M11 parity workflows
This commit is contained in:
@@ -30,10 +30,12 @@ import type { MeshElementMode, MeshGeometryBuffer, WebEngineLODLevelResult } fro
|
||||
import { ThreeLODAdapter, type ThreeLODLevel, type LODSelectionResult } from "./lod";
|
||||
import {
|
||||
configurePBRLight,
|
||||
configurePBRCamera,
|
||||
configurePBRRenderer,
|
||||
createPBRLight,
|
||||
createPBRMaterial,
|
||||
PBR_PROFILE,
|
||||
PBR_SHADOW_MAP_DIMENSION,
|
||||
PBR_SHADOW_PROFILE,
|
||||
PBR_TONE_MAPPING,
|
||||
setPBRMaterialSelected,
|
||||
@@ -41,6 +43,7 @@ import {
|
||||
import { GPUTextureStore } from "./texture-assets";
|
||||
import type { GPUTextureAsset } from "../../../protocol/render-assets";
|
||||
import { gateEnvironmentImage, gateUDIMImage } from "../../../protocol/render-assets";
|
||||
import { planPBRLightingBudget, type PBRLightingBudgetReport } from "../../../protocol/render-budget";
|
||||
import { applyCurveHandlePreview, applyNonMeshElementSelection, applyNonMeshTransform, createNonMeshObject, type NonMeshElementKind } from "./nonmesh";
|
||||
import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary";
|
||||
import type { CurveGizmoFrameIR, CurveGizmoHandleIR, CurveGizmoScreenFrameIR } from "../../../protocol/nonmesh-interaction";
|
||||
@@ -52,10 +55,20 @@ import {
|
||||
applyGreasePencilPointPreview,
|
||||
applyGreasePencilTransform,
|
||||
createGreasePencilObject,
|
||||
greasePencilMarqueeCandidates,
|
||||
greasePencilPointRef,
|
||||
type GreasePencilPointRef,
|
||||
type GreasePencilPointPreview,
|
||||
} from "./grease-pencil";
|
||||
import {
|
||||
selectGreasePencilMarquee,
|
||||
type GreasePencilDrawingScopeIR,
|
||||
type GreasePencilMarqueeBoxIR,
|
||||
type GreasePencilMarqueeResultIR,
|
||||
} from "../../../protocol/grease-pencil-marquee";
|
||||
import { VIEWPORT_DEFAULT_ORBIT, VIEWPORT_ORBIT_MAX_DISTANCE, VIEWPORT_ORBIT_MIN_DISTANCE, VIEWPORT_ORBIT_ROTATE_SENSITIVITY, VIEWPORT_ORBIT_ZOOM_SENSITIVITY, orbitPosition, orbitStateFromPosition } from "../../../protocol/viewport-camera";
|
||||
import { validatePaintDepthVisibilityRequest, type PaintDepthVisibilityRequestIR, type PaintDepthVisibilityResultIR } from "../../../protocol/paint-depth-visibility";
|
||||
import { samplePaintDepthVisibilityGPU } from "./paint-depth-visibility";
|
||||
|
||||
export function collectMeshInstanceGroups(snapshot: SceneSnapshotIR, minimumSize = 2): Map<string, string[]> {
|
||||
const groups = new Map<string, string[]>();
|
||||
@@ -86,14 +99,16 @@ export class ViewportRenderer {
|
||||
private readonly objectByBlenderId = new Map<string, Object3D>();
|
||||
private readonly instanceIndexByBlenderId = new Map<string, number>();
|
||||
private readonly lodAdapter = new ThreeLODAdapter();
|
||||
private readonly textureStore = new GPUTextureStore();
|
||||
private readonly textureStore = new GPUTextureStore("THREE_WEBGL2");
|
||||
private readonly raycaster = new Raycaster();
|
||||
private readonly pointer = new Vector2();
|
||||
private readonly onSelect?: (objectId: string, additive: boolean) => void;
|
||||
private readonly onElementSelect?: (meshId: string, mode: MeshElementMode, index: number, additive: boolean, nonMeshKind?: NonMeshElementKind) => void;
|
||||
private readonly onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean) => void;
|
||||
private readonly onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean, baseSelectionRevision: number) => void;
|
||||
private readonly onGreasePencilMarqueeSelect?: (result: GreasePencilMarqueeResultIR, additive: boolean) => void;
|
||||
private editMode = false;
|
||||
private selectionMode: MeshElementMode = "FACE";
|
||||
private greasePencilSelectionRevision = 0;
|
||||
private curveGizmoFrame: { dataId: string; frame: CurveGizmoFrameIR } | null = null;
|
||||
private curveGizmoScreenFrame = "";
|
||||
private volumeAssets: NanoVDBViewportAssetIR[] = [];
|
||||
@@ -105,12 +120,14 @@ export class ViewportRenderer {
|
||||
canvas: HTMLCanvasElement,
|
||||
onSelect?: (objectId: string, additive: boolean) => void,
|
||||
onElementSelect?: (meshId: string, mode: MeshElementMode, index: number, additive: boolean, nonMeshKind?: NonMeshElementKind) => void,
|
||||
onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean) => void,
|
||||
onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean, baseSelectionRevision: number) => void,
|
||||
onGreasePencilMarqueeSelect?: (result: GreasePencilMarqueeResultIR, additive: boolean) => void,
|
||||
) {
|
||||
this.canvas = canvas;
|
||||
this.onSelect = onSelect;
|
||||
this.onElementSelect = onElementSelect;
|
||||
this.onGreasePencilPointSelect = onGreasePencilPointSelect;
|
||||
this.onGreasePencilMarqueeSelect = onGreasePencilMarqueeSelect;
|
||||
this.volumeRenderSession = new NanoVDBViewportRenderSession(() => {
|
||||
if (this.disposed) return;
|
||||
this.volumeRenderCache.clear();
|
||||
@@ -129,16 +146,20 @@ export class ViewportRenderer {
|
||||
this.canvas.dataset.deviceStatus = "ready";
|
||||
this.scene = new Scene();
|
||||
this.camera = new PerspectiveCamera(45, 1, 0.01, 1000);
|
||||
this.camera.position.set(4.5, -4.5, 3.5);
|
||||
this.camera.position.set(...orbitPosition(VIEWPORT_DEFAULT_ORBIT));
|
||||
this.controls = new OrbitControls(this.camera, canvas);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.target.set(...VIEWPORT_DEFAULT_ORBIT.target);
|
||||
this.controls.enableDamping = false;
|
||||
this.controls.enablePan = false;
|
||||
this.controls.minDistance = VIEWPORT_ORBIT_MIN_DISTANCE;
|
||||
this.controls.maxDistance = VIEWPORT_ORBIT_MAX_DISTANCE;
|
||||
this.controls.zoomSpeed = VIEWPORT_ORBIT_ZOOM_SENSITIVITY / (0.01 * -Math.log(0.95));
|
||||
|
||||
this.scene.add(new HemisphereLight(0xf2f5ff, 0x3a4149, 0.55));
|
||||
const keyLight = new DirectionalLight(0xffffff, 2.5);
|
||||
keyLight.position.set(4, -5, 8);
|
||||
keyLight.castShadow = true;
|
||||
keyLight.shadow.mapSize.set(1024, 1024);
|
||||
keyLight.shadow.mapSize.set(PBR_SHADOW_MAP_DIMENSION, PBR_SHADOW_MAP_DIMENSION);
|
||||
keyLight.shadow.bias = -0.0005;
|
||||
keyLight.shadow.normalBias = 0.03;
|
||||
this.scene.add(keyLight, keyLight.target);
|
||||
@@ -152,6 +173,8 @@ export class ViewportRenderer {
|
||||
this.canvas.addEventListener("webglcontextlost", this.handleContextLost);
|
||||
this.canvas.addEventListener("webglcontextrestored", this.handleContextRestored);
|
||||
this.resize();
|
||||
this.controls.update();
|
||||
this.publishCameraState();
|
||||
this.renderLoop();
|
||||
}
|
||||
|
||||
@@ -176,7 +199,9 @@ export class ViewportRenderer {
|
||||
this.clearImportedScene();
|
||||
this.applyWorld(snapshot);
|
||||
this.applyCamera(snapshot);
|
||||
this.populateLights(snapshot);
|
||||
const lightingBudget = planPBRLightingBudget(snapshot, "THREE_WEBGL2");
|
||||
this.publishLightingBudget(lightingBudget);
|
||||
this.populateLights(snapshot, lightingBudget);
|
||||
this.populateNonMesh(snapshot, nonMeshGeometryBuffers);
|
||||
this.populateGreasePencils(snapshot);
|
||||
const meshes = new Map(snapshot.meshes.map((mesh) => [mesh.id, mesh]));
|
||||
@@ -340,6 +365,11 @@ export class ViewportRenderer {
|
||||
this.canvas.dataset.textureStatus = "none";
|
||||
this.canvas.dataset.textureLoaded = "0";
|
||||
this.canvas.dataset.textureBytes = "0";
|
||||
this.canvas.dataset.textureBudgetStatus = "ready";
|
||||
this.canvas.dataset.textureBudgetCode = "";
|
||||
this.canvas.dataset.textureBudgetAssets = "0";
|
||||
this.canvas.dataset.textureBudgetPayloadBytes = "0";
|
||||
this.canvas.dataset.textureBudgetGpuBytes = "0";
|
||||
return;
|
||||
}
|
||||
void this.textureStore.upload(assets).then((status) => {
|
||||
@@ -347,6 +377,11 @@ export class ViewportRenderer {
|
||||
this.canvas.dataset.textureLoaded = String(status.loaded);
|
||||
this.canvas.dataset.textureBytes = String(status.bytes);
|
||||
this.canvas.dataset.textureErrorCode = status.errorCodes[0] ?? "";
|
||||
this.canvas.dataset.textureBudgetStatus = status.budget.status.toLowerCase();
|
||||
this.canvas.dataset.textureBudgetCode = status.budget.issues[0]?.code ?? "";
|
||||
this.canvas.dataset.textureBudgetAssets = String(status.budget.requestedAssets);
|
||||
this.canvas.dataset.textureBudgetPayloadBytes = String(status.budget.payloadBytes);
|
||||
this.canvas.dataset.textureBudgetGpuBytes = String(status.budget.decodedGPUBytes);
|
||||
if (!this.currentSnapshot) return;
|
||||
this.textureStore.applySnapshotMaterials(this.importedRoot, this.currentSnapshot);
|
||||
const worldId = this.currentSnapshot.scenes[0]?.worldId;
|
||||
@@ -449,6 +484,7 @@ export class ViewportRenderer {
|
||||
objectIds: ReadonlySet<string>,
|
||||
elementSelection?: ReadonlyMap<string, ReadonlyMap<NonMeshElementKind, ReadonlySet<number>>>,
|
||||
greasePencilPoints: readonly GreasePencilPointRef[] = [],
|
||||
greasePencilSelectionRevision = 0,
|
||||
): void {
|
||||
const visitedInstances = new Set<InstancedMesh>();
|
||||
for (const [objectId, object] of this.objectByBlenderId) {
|
||||
@@ -471,6 +507,9 @@ export class ViewportRenderer {
|
||||
}
|
||||
applyNonMeshElementSelection(this.importedRoot, elementSelection ?? new Map());
|
||||
applyGreasePencilPointSelection(this.importedRoot, greasePencilPoints);
|
||||
this.greasePencilSelectionRevision = greasePencilSelectionRevision;
|
||||
this.canvas.dataset.greasePencilSelectionRevision = String(greasePencilSelectionRevision);
|
||||
this.canvas.dataset.greasePencilSelectionPointIds = greasePencilPoints.map((point) => point.pointId).join(",");
|
||||
}
|
||||
|
||||
setInteractionMode(editMode: boolean, selectionMode: MeshElementMode): void {
|
||||
@@ -481,6 +520,44 @@ export class ViewportRenderer {
|
||||
});
|
||||
}
|
||||
|
||||
async samplePaintVisibility(requestValue: PaintDepthVisibilityRequestIR): Promise<PaintDepthVisibilityResultIR> {
|
||||
const request = validatePaintDepthVisibilityRequest(requestValue, this.currentSnapshot?.revision ?? -1);
|
||||
const node = this.currentSnapshot?.nodes.find((candidate) => candidate.id === request.objectId && candidate.dataId === request.meshId && candidate.visible);
|
||||
const object = node ? this.objectByBlenderId.get(node.id) : undefined;
|
||||
if (!object) throw new Error("PAINT_DEPTH_UNAVAILABLE: Paint object is not available in the current viewport");
|
||||
return samplePaintDepthVisibilityGPU({
|
||||
renderer: this.renderer,
|
||||
scene: this.scene,
|
||||
camera: this.camera,
|
||||
object,
|
||||
request,
|
||||
backend: "MAIN_THREAD_WEBGL2",
|
||||
});
|
||||
}
|
||||
|
||||
selectGreasePencilMarquee(
|
||||
drawing: GreasePencilDrawingScopeIR,
|
||||
box: GreasePencilMarqueeBoxIR,
|
||||
baseRevision: number,
|
||||
baseSelectionRevision: number,
|
||||
additive: boolean,
|
||||
): void {
|
||||
const result = selectGreasePencilMarquee({
|
||||
schemaVersion: 1,
|
||||
baseRevision,
|
||||
baseSelectionRevision,
|
||||
drawing,
|
||||
box,
|
||||
candidates: greasePencilMarqueeCandidates(this.importedRoot, drawing, this.camera),
|
||||
}, this.currentSnapshot?.revision ?? -1);
|
||||
this.canvas.dataset.greasePencilMarqueeSelectionRevision = String(result.baseSelectionRevision);
|
||||
this.canvas.dataset.greasePencilMarqueeDrawingId = result.drawing.drawingId;
|
||||
this.canvas.dataset.greasePencilMarqueePointIds = result.selectedPoints.map((point) => point.pointId).join(",");
|
||||
this.canvas.dataset.greasePencilMarqueeStrokeIds = result.selectedStrokeIds.join(",");
|
||||
this.canvas.dataset.greasePencilMarqueeCount = String(result.selectedPoints.length);
|
||||
this.onGreasePencilMarqueeSelect?.(result, additive);
|
||||
}
|
||||
|
||||
setCurveHandlePreview(dataId: string, handles: readonly CurveGizmoHandleIR[] | null): void {
|
||||
applyCurveHandlePreview(this.importedRoot, dataId, handles);
|
||||
this.canvas.dataset.curveGizmoPreview = handles ? String(handles.length) : "0";
|
||||
@@ -675,24 +752,35 @@ export class ViewportRenderer {
|
||||
const cameraNode = snapshot.nodes.find((node) => node.id === cameraObjectId && node.type === "CAMERA");
|
||||
const definition = snapshot.cameras.find((camera) => camera.id === cameraNode?.dataId);
|
||||
if (!cameraNode || !definition) return;
|
||||
const sensor = definition.sensorFit === 2 ? definition.sensorHeightMm : definition.sensorWidthMm;
|
||||
const fov = (2 * Math.atan((sensor / Math.max(0.001, definition.lensMm)) / 2) * 180) / Math.PI;
|
||||
this.camera.fov = definition.projection === "ORTHOGRAPHIC" ? 45 : fov;
|
||||
this.camera.near = Math.max(0.0001, definition.near);
|
||||
this.camera.far = Math.max(this.camera.near + 0.001, definition.far);
|
||||
this.camera.filmGauge = sensor;
|
||||
this.camera.filmOffset = definition.shift[0] * sensor;
|
||||
this.camera.updateProjectionMatrix();
|
||||
configurePBRCamera(this.camera, definition);
|
||||
}
|
||||
|
||||
private populateLights(snapshot: SceneSnapshotIR): void {
|
||||
private publishLightingBudget(report: PBRLightingBudgetReport): void {
|
||||
this.canvas.dataset.renderBudgetBackend = report.backend;
|
||||
this.canvas.dataset.renderBudgetStatus = report.status.toLowerCase();
|
||||
this.canvas.dataset.renderBudgetCode = report.issues[0]?.code ?? "";
|
||||
this.canvas.dataset.renderBudgetLights = String(report.requestedLights);
|
||||
this.canvas.dataset.renderBudgetRenderedLights = String(report.renderedLightNodeIds.length);
|
||||
this.canvas.dataset.renderBudgetDroppedLights = String(report.droppedLightNodeIds.length);
|
||||
this.canvas.dataset.renderBudgetShadows = String(report.requestedShadowMaps);
|
||||
this.canvas.dataset.renderBudgetRenderedShadows = String(report.shadowLightNodeIds.length);
|
||||
this.canvas.dataset.renderBudgetBlockedShadows = String(report.shadowBlockedLightNodeIds.length);
|
||||
this.canvas.dataset.renderBudgetShadowMapDimension = String(report.budget.shadowMapDimension);
|
||||
}
|
||||
|
||||
private populateLights(snapshot: SceneSnapshotIR, budget = planPBRLightingBudget(snapshot, "THREE_WEBGL2")): void {
|
||||
const lights = new Map(snapshot.lights.map((light) => [light.id, light]));
|
||||
const rendered = new Set(budget.renderedLightNodeIds);
|
||||
const shadowed = new Set(budget.shadowLightNodeIds);
|
||||
for (const node of snapshot.nodes) {
|
||||
if (node.type !== "LIGHT" || !node.visible || !node.dataId) continue;
|
||||
if (node.type !== "LIGHT" || !node.visible || !node.dataId || !rendered.has(node.id)) continue;
|
||||
const definition = lights.get(node.dataId);
|
||||
if (!definition) continue;
|
||||
const light = createPBRLight(definition);
|
||||
configurePBRLight(light, node, this.importedLights);
|
||||
configurePBRLight(light, node, this.importedLights, {
|
||||
shadowEnabled: shadowed.has(node.id),
|
||||
shadowMapDimension: budget.budget.shadowMapDimension,
|
||||
});
|
||||
light.name = node.name;
|
||||
light.userData.sceneNodeId = node.id;
|
||||
light.userData.blenderId = node.id;
|
||||
@@ -707,7 +795,18 @@ export class ViewportRenderer {
|
||||
const height = Math.max(1, this.canvas.clientHeight);
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.controls.rotateSpeed = VIEWPORT_ORBIT_ROTATE_SENSITIVITY * height / (2 * Math.PI);
|
||||
this.renderer.setSize(width, height, false);
|
||||
this.publishCameraState();
|
||||
}
|
||||
|
||||
private publishCameraState(): void {
|
||||
const orbit = orbitStateFromPosition(this.camera.position.toArray(), this.controls.target.toArray());
|
||||
this.canvas.dataset.cameraPosition = this.camera.position.toArray().map((value) => Number(value.toFixed(6))).join(",");
|
||||
this.canvas.dataset.cameraTarget = orbit.target.map((value) => Number(value.toFixed(6))).join(",");
|
||||
this.canvas.dataset.cameraYaw = orbit.yaw.toFixed(6);
|
||||
this.canvas.dataset.cameraPitch = orbit.pitch.toFixed(6);
|
||||
this.canvas.dataset.cameraDistance = orbit.distance.toFixed(6);
|
||||
}
|
||||
|
||||
private handleClick = (event: MouseEvent): void => {
|
||||
@@ -724,7 +823,7 @@ export class ViewportRenderer {
|
||||
: undefined;
|
||||
if (greasePencilHit?.index !== undefined) {
|
||||
const point = greasePencilPointRef(greasePencilHit.object, greasePencilHit.index);
|
||||
if (point) this.onGreasePencilPointSelect?.(point, event.shiftKey || event.ctrlKey || event.metaKey);
|
||||
if (point) this.onGreasePencilPointSelect?.(point, event.shiftKey || event.ctrlKey || event.metaKey, this.greasePencilSelectionRevision);
|
||||
return;
|
||||
}
|
||||
const preferredNonMeshHit = this.editMode ? hits.find((intersection) => intersection.index !== undefined && Array.isArray(intersection.object.userData.nonMeshPointKindMap)) : undefined;
|
||||
@@ -791,6 +890,7 @@ export class ViewportRenderer {
|
||||
if (this.disposed) return;
|
||||
if (!this.contextLost) {
|
||||
this.controls.update();
|
||||
this.publishCameraState();
|
||||
this.lodAdapter.update(this.camera, Math.max(1, this.canvas.clientHeight));
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
this.publishCurveGizmoFrame();
|
||||
|
||||
Reference in New Issue
Block a user