feat(web): add world lightgroup membership
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-30 07:34:08 +08:00
parent cc8f805e76
commit 63ba5b1e0c
46 changed files with 1901 additions and 36 deletions

Binary file not shown.

View File

@@ -938,7 +938,7 @@ function Properties({ snapshot, selectedFaceIndices, selectedVertexIndices, grea
{(snapshot?.libraries?.length ?? 0) > 0 ? <div className="property-section" data-testid="library-append-workbench"><h3>Library Append</h3>{snapshot?.libraries?.slice(0, 16).map((library) => <output key={library.id} data-resource-id={library.id}>{library.name} · {library.status} · {library.sourcePath}</output>)}<label>Source .blend <input aria-label="Library source blend" type="file" accept=".blend,application/octet-stream" onChange={(event) => { setLibrarySourceFile(event.target.files?.[0] ?? null); setLibraryAppendError(""); }} /></label><label>Binding JSON <input aria-label="Library append binding" type="file" accept="application/json,.json" onChange={(event) => { setLibraryRequestFile(event.target.files?.[0] ?? null); setLibraryAppendError(""); }} /></label><button type="button" disabled={!librarySourceFile || !libraryRequestFile} onClick={() => void appendLibrary()}>Append to Main</button>{libraryAppendError ? <output role="alert">{libraryAppendError}</output> : null}</div> : null}
{activeCamera ? <div className="property-section" data-testid="camera-workbench"><h3>Camera</h3><output>{activeCamera.projection} · {activeCamera.lensMm.toFixed(1)}mm</output><button type="button" onClick={() => onCommand(animationRenderCommands.setCameraProperties(activeCamera.id, { projection: activeCamera.projection === "ORTHOGRAPHIC" ? "PERSPECTIVE" : "ORTHOGRAPHIC" }))}>切换投影</button><button type="button" onClick={() => onCommand(animationRenderCommands.setCameraProperties(activeCamera.id, { lensMm: Math.min(10000, activeCamera.lensMm + 5) }))}>增加焦距</button></div> : null}
{activeLight ? <div className="property-section" data-testid="light-workbench"><h3>Light</h3><output>{activeLight.energy.toFixed(1)} energy</output><button type="button" onClick={() => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { energy: Math.min(1e9, activeLight.energy + 10) }))}>增加能量</button><button type="button" onClick={() => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { castsShadow: !(activeLight.castsShadow ?? true) }))}>切换阴影</button></div> : null}
{activeWorld ? <div className="property-section" data-testid="world-workbench"><h3>World</h3><output>Exposure {activeWorld.exposure.toFixed(2)}</output><button type="button" onClick={() => onCommand(animationRenderCommands.setWorldProperties(activeWorld.id, { exposure: Math.max(-20, Math.min(20, activeWorld.exposure + 0.5)) }))}>增加曝光</button></div> : null}
{activeWorld ? <div className="property-section" data-testid="world-workbench"><h3>World</h3><output>Exposure {activeWorld.exposure.toFixed(2)} · Lightgroup {activeWorld.lightgroup ?? "None"}</output><div className="property-actions"><button type="button" onClick={() => onCommand(animationRenderCommands.setWorldProperties(activeWorld.id, { exposure: Math.max(-20, Math.min(20, activeWorld.exposure + 0.5)) }))}>增加曝光</button><button type="button" onClick={() => onCommand(animationRenderCommands.setWorldLightgroup(activeWorld.id, activeWorld.lightgroup ? null : "Lightgroup_1"))}>{activeWorld.lightgroup ? "清除分组" : "绑定分组"}</button></div></div> : null}
{activeScene ? <div className="property-section" data-testid="scene-properties-workbench"><h3>Scene Render</h3><output>{activeScene.name} · {activeScene.renderEngine ?? "UNKNOWN"}</output><output>{activeScene.colorManagement ? `${activeScene.colorManagement.viewTransform} · ${activeScene.colorManagement.look} · Exposure ${activeScene.colorManagement.exposure.toFixed(2)} · Gamma ${activeScene.colorManagement.gamma.toFixed(2)}` : "Color management unavailable"}</output><div className="property-actions"><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneProperties(activeScene.id, { exposure: Math.max(-1000, Math.min(1000, (activeScene.colorManagement?.exposure ?? 0) + 0.5)) }))}>增加曝光</button><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneProperties(activeScene.id, { gamma: Math.max(0, Math.min(1000, (activeScene.colorManagement?.gamma ?? 1) + 0.1)) }))}>增加 Gamma</button><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneProperties(activeScene.id, { viewTransform: activeScene.colorManagement?.viewTransform === "AgX" ? "Standard" : "AgX" }))}>切换 View Transform</button></div></div> : null}
{activeScene?.colorManagement ? <div className="property-section" data-testid="scene-color-management-workbench"><h3>Color Management</h3><output>{activeScene.colorManagement.displayDevice} · {activeScene.colorManagement.temperature?.toFixed(0) ?? "-"} K · Tint {activeScene.colorManagement.tint?.toFixed(2) ?? "-"}</output><div className="property-actions"><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneColorManagement(activeScene.id, { temperature: Math.min(20000, (activeScene.colorManagement!.temperature ?? 6500) + 250) }))}>升高色温</button><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneColorManagement(activeScene.id, { tint: Math.max(-100, Math.min(100, (activeScene.colorManagement!.tint ?? 0) + 1)) }))}>调整 Tint</button></div></div> : null}
{activeScene?.imageFormatSettings ? <div className="property-section" data-testid="scene-image-format-workbench"><h3>Output Format</h3><output>{activeScene.imageFormatSettings.fileType} · {activeScene.imageFormatSettings.colorMode}-channel · {activeScene.imageFormatSettings.depth}-bit · Quality {activeScene.imageFormatSettings.quality}</output><div className="property-actions"><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneImageFormat(activeScene.id, { fileType: activeScene.imageFormatSettings!.fileType === "PNG" ? "OPEN_EXR" : "PNG" }))}>切换格式</button><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneImageFormat(activeScene.id, { colorMode: activeScene.imageFormatSettings!.colorMode === 32 ? 24 : 32 }))}>切换通道</button><button type="button" onClick={() => onCommand(animationRenderCommands.setSceneImageFormat(activeScene.id, { quality: Math.min(100, activeScene.imageFormatSettings!.quality + 5) }))}>提高质量</button></div></div> : null}

Binary file not shown.

View File

@@ -834,6 +834,12 @@ function assertFutureCapability(payload: Extract<WebEngineRequest["command"], {
if (payload.lightgroup !== null && (payload.lightgroup.length === 0 || new TextEncoder().encode(payload.lightgroup).byteLength > 63)) throw report("RENDER_PROPERTY_INVALID", "Object lightgroup name is not bounded");
return;
}
case "setWorldLightgroup": {
const world = currentSnapshot?.worlds.find((candidate) => candidate.id === payload.dataId);
if (!world || typeof payload.lightgroup !== "string" && payload.lightgroup !== null) throw report("RENDER_PROPERTY_INVALID", "World lightgroup target is invalid");
if (payload.lightgroup !== null && (payload.lightgroup.length === 0 || new TextEncoder().encode(payload.lightgroup).byteLength > 63)) throw report("RENDER_PROPERTY_INVALID", "World lightgroup name is not bounded");
return;
}
case "setGeometryNodeGraph": {
const snapshot = currentSnapshot;
const availableResourceIds = new Set([

View File

@@ -104,4 +104,7 @@ export const animationRenderCommands = Object.freeze({
setObjectLightgroup(objectId: string, lightgroup: string | null): AnimationCommand<"setObjectLightgroup"> {
return { type: "setObjectLightgroup", objectId, lightgroup };
},
setWorldLightgroup(dataId: string, lightgroup: string | null): AnimationCommand<"setWorldLightgroup"> {
return { type: "setWorldLightgroup", dataId, lightgroup };
},
} satisfies Record<string, (...args: never[]) => WebEngineEditCommand>);

View File

@@ -268,6 +268,7 @@ export interface WorldIR {
environmentStrength?: number;
environmentRotation?: number;
backgroundVisible?: boolean;
lightgroup?: string | null;
mist?: {
enabled: boolean;
type: "QUADRATIC" | "LINEAR" | "INVERSE_QUADRATIC";
@@ -1552,6 +1553,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
requireString(world.name, `worlds[${index}].name`);
requireTuple(world.color, 3, `worlds[${index}].color`);
requireNumber(world.exposure, `worlds[${index}].exposure`);
if (world.lightgroup !== undefined && world.lightgroup !== null) requireString(world.lightgroup, `worlds[${index}].lightgroup`);
if (world.environmentImageId !== undefined && world.environmentImageId !== null) requireString(world.environmentImageId, `worlds[${index}].environmentImageId`);
if (world.environmentStrength !== undefined) requireNumber(world.environmentStrength, `worlds[${index}].environmentStrength`);
if (world.environmentRotation !== undefined) requireNumber(world.environmentRotation, `worlds[${index}].environmentRotation`);

View File

@@ -104,6 +104,7 @@ export const WEB_ENGINE_COMMAND_CAPABILITIES = {
setSceneViewLayerAOVs: { family: "RENDER", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setSceneViewLayerLightgroups: { family: "RENDER", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setObjectLightgroup: { family: "RENDER", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setWorldLightgroup: { family: "RENDER", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setMetaballElements: { family: "GEOMETRY", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
sculptStroke: { family: "SCULPT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },
setSculptMeshAttributes: { family: "SCULPT", route: "MAIN_AUTHORITATIVE", implementation: "SOURCE_BACKED" },

View File

@@ -168,6 +168,7 @@ export type WebEngineEditCommand =
| { type: "setSceneViewLayerAOVs"; dataId: string; layerName: string; aovs: Array<{ name: string; type: 0 | 1 }> }
| { type: "setSceneViewLayerLightgroups"; dataId: string; layerName: string; lightgroups: string[] }
| { type: "setObjectLightgroup"; objectId: string; lightgroup: string | null }
| { type: "setWorldLightgroup"; dataId: string; lightgroup: string | null }
| { type: "setMetaballElements"; dataId: string; elements: Array<{ type: number; position: [number, number, number]; radius: number; scale: [number, number, number] }> }
| { type: "sculptStroke"; stroke: SculptStrokeIR }
| { type: "setSculptMeshAttributes"; attributes: SculptMeshAttributesIR }

View File

@@ -4,7 +4,7 @@ export const WORKBENCH_COMMAND_GROUPS = Object.freeze({
OBJECT: ["createPrimitive", "duplicateObject", "deleteObject", "setObjectTransform", "setParent", "createCollection", "moveObjectToCollection", "renameId", "applyObjectTransform", "setObjectOrigin"],
MESH: ["meshEdit", "translateMeshVertices", "createUVMap", "setActiveUVMap", "unwrapUV", "setVertexColors", "setVertexWeights"],
ANIMATION: ["setFrame", "insertObjectKeyframe", "deleteObjectKeyframe", "setFCurveInterpolation", "setConstraint", "setNLAStack"],
RENDER: ["setCameraProperties", "setLightProperties", "setWorldProperties", "setSceneProperties", "setSceneRenderSettings", "setSceneAudioSettings", "setSceneUnitSettings", "setSceneTimelineSettings", "setScenePipelineSettings", "setSceneBindings", "setSceneViewLayerSettings", "setSceneCollectionSettings", "setSceneBorderSettings", "setSceneSimplifySettings", "setSceneColorManagement", "setSceneImageFormat", "setSceneMotionBlur", "setSceneHairSettings", "setSceneCompositorSettings", "setScenePerformanceSettings", "setSceneSamplingSettings", "setSceneMultiviewSettings", "setSceneStampSettings", "setSceneLineSettings", "setSceneViewLayerPassSettings", "setSceneViewLayerAOVs", "setSceneViewLayerLightgroups", "setObjectLightgroup"],
RENDER: ["setCameraProperties", "setLightProperties", "setWorldProperties", "setSceneProperties", "setSceneRenderSettings", "setSceneAudioSettings", "setSceneUnitSettings", "setSceneTimelineSettings", "setScenePipelineSettings", "setSceneBindings", "setSceneViewLayerSettings", "setSceneCollectionSettings", "setSceneBorderSettings", "setSceneSimplifySettings", "setSceneColorManagement", "setSceneImageFormat", "setSceneMotionBlur", "setSceneHairSettings", "setSceneCompositorSettings", "setScenePerformanceSettings", "setSceneSamplingSettings", "setSceneMultiviewSettings", "setSceneStampSettings", "setSceneLineSettings", "setSceneViewLayerPassSettings", "setSceneViewLayerAOVs", "setSceneViewLayerLightgroups", "setObjectLightgroup", "setWorldLightgroup"],
IO: ["importImage", "importVFont", "setMaterialImageNode"],
} as const satisfies Record<string, readonly WebEngineCommandType[]>);