feat(web): wire Blender light contribution factors
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 18:36:55 +08:00
parent 2f00b3e322
commit 8bc5d61b75
41 changed files with 1967 additions and 24 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -937,7 +937,7 @@ function Properties({ snapshot, selectedFaceIndices, selectedVertexIndices, grea
{activeTexture ? <div className="property-section" data-testid="texture-properties-workbench"><h3>Texture</h3><output>{activeTexture.name} · Type {activeTexture.type}</output><output>Noise {activeTexture.noiseScale.toFixed(3)} · Depth {activeTexture.noiseDepth}</output><div className="property-actions"><button type="button" onClick={() => onCommand(ioMediaCommands.setTextureProperties(activeTexture.id, { noiseScale: Math.min(1000000, activeTexture.noiseScale + 0.05) }))}>Increase Noise</button><button type="button" onClick={() => onCommand(ioMediaCommands.setTextureProperties(activeTexture.id, { noiseDepth: Math.min(30, activeTexture.noiseDepth + 1) }))}>Increase Depth</button></div></div> : null}
{(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><label>类型 <select aria-label="灯光类型" value={({ 0: "POINT", 1: "SUN", 2: "SPOT", 4: "AREA" } as Record<number, string>)[activeLight.lightType] ?? "POINT"} onChange={(event) => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { type: event.target.value as "POINT" | "SUN" | "SPOT" | "AREA" }))}><option value="POINT">Point</option><option value="SUN">Sun</option><option value="SPOT">Spot</option><option value="AREA">Area</option></select></label><label>Area Shape <select aria-label="灯光区域形状" value={activeLight.areaShape} onChange={(event) => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { areaShape: Number(event.target.value) as 0 | 1 | 4 | 5 }))}><option value="0">Square</option><option value="1">Rectangle</option><option value="4">Disk</option><option value="5">Ellipse</option></select></label><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}
{activeLight ? <div className="property-section" data-testid="light-workbench"><h3>Light</h3><output>{activeLight.energy.toFixed(1)} energy</output><label>类型 <select aria-label="灯光类型" value={({ 0: "POINT", 1: "SUN", 2: "SPOT", 4: "AREA" } as Record<number, string>)[activeLight.lightType] ?? "POINT"} onChange={(event) => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { type: event.target.value as "POINT" | "SUN" | "SPOT" | "AREA" }))}><option value="POINT">Point</option><option value="SUN">Sun</option><option value="SPOT">Spot</option><option value="AREA">Area</option></select></label><label>Area Shape <select aria-label="灯光区域形状" value={activeLight.areaShape} onChange={(event) => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { areaShape: Number(event.target.value) as 0 | 1 | 4 | 5 }))}><option value="0">Square</option><option value="1">Rectangle</option><option value="4">Disk</option><option value="5">Ellipse</option></select></label><output>贡献因子 D {(activeLight.diffuseFactor ?? 1).toFixed(2)} · S {(activeLight.specularFactor ?? 1).toFixed(2)} · T {(activeLight.transmissionFactor ?? 1).toFixed(2)} · V {(activeLight.volumeFactor ?? 1).toFixed(2)}</output><div className="property-actions"><button type="button" onClick={() => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { diffuseFactor: Math.min(1, (activeLight.diffuseFactor ?? 1) + 0.1) }))}>提高漫反射</button><button type="button" onClick={() => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { specularFactor: Math.min(1, (activeLight.specularFactor ?? 1) + 0.1) }))}>提高镜面</button><button type="button" onClick={() => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { transmissionFactor: Math.min(1, (activeLight.transmissionFactor ?? 1) + 0.1) }))}>提高透射</button><button type="button" onClick={() => onCommand(animationRenderCommands.setLightProperties(activeLight.id, { volumeFactor: Math.min(1, (activeLight.volumeFactor ?? 1) + 0.1) }))}>提高体积</button></div><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)} · 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}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -541,9 +541,10 @@ function assertFutureCapability(payload: Extract<WebEngineRequest["command"], {
case "setLightProperties": {
const light = currentSnapshot?.lights.find((candidate) => candidate.id === payload.dataId);
const properties = payload.properties;
if (!light || !validPropertyKeys(properties, ["type", "areaShape", "color", "energy", "exposure", "temperature", "useTemperature", "castsShadow", "radius", "spotAngle", "spotBlend", "areaSize", "areaSizeY", "areaSpread", "sunAngle"])) throw report("RENDER_PROPERTY_INVALID", "Light target or properties are invalid");
if (!light || !validPropertyKeys(properties, ["type", "areaShape", "diffuseFactor", "specularFactor", "transmissionFactor", "volumeFactor", "color", "energy", "exposure", "temperature", "useTemperature", "castsShadow", "radius", "spotAngle", "spotBlend", "areaSize", "areaSizeY", "areaSpread", "sunAngle"])) throw report("RENDER_PROPERTY_INVALID", "Light target or properties are invalid");
if (properties.type !== undefined && !["POINT", "SUN", "SPOT", "AREA"].includes(properties.type)) throw report("RENDER_PROPERTY_INVALID", "Light type is invalid");
if (properties.areaShape !== undefined && ![0, 1, 4, 5].includes(properties.areaShape)) throw report("RENDER_PROPERTY_INVALID", "Light area shape is invalid");
for (const field of ["diffuseFactor", "specularFactor", "transmissionFactor", "volumeFactor"] as const) if (properties[field] !== undefined && !boundedNumber(properties[field], 0, 1)) throw report("RENDER_PROPERTY_INVALID", `Light ${field} is outside the bounded range`);
if (properties.color !== undefined && (!Array.isArray(properties.color) || properties.color.length !== 3 || properties.color.some((value) => !boundedNumber(value, 0, 100)))) throw report("RENDER_PROPERTY_INVALID", "Light color is invalid");
for (const [field, minimum, maximum] of [["energy", 0, 1e9], ["exposure", -20, 20], ["temperature", 800, 20000], ["radius", 0, 1e9], ["spotAngle", 0.0001, Math.PI], ["spotBlend", 0, 1], ["areaSize", 0.0001, 1e9], ["areaSizeY", 0.0001, 1e9], ["areaSpread", 0.0001, Math.PI], ["sunAngle", 0, Math.PI]] as const) {
const value = properties[field];

View File

@@ -252,6 +252,10 @@ export interface LightIR {
areaSize: number;
areaSizeY: number;
sunAngle: number;
diffuseFactor?: number;
specularFactor?: number;
transmissionFactor?: number;
volumeFactor?: number;
exposure?: number;
temperature?: number;
useTemperature?: boolean;
@@ -1544,7 +1548,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
for (const field of ["energy", "radius", "spotAngle", "spotBlend", "areaShape", "areaSize", "areaSizeY", "sunAngle"]) {
requireNumber(light[field], `lights[${index}].${field}`);
}
for (const field of ["exposure", "temperature", "areaSpread"] as const) if (light[field] !== undefined) requireNumber(light[field], `lights[${index}].${field}`);
for (const field of ["exposure", "temperature", "areaSpread", "diffuseFactor", "specularFactor", "transmissionFactor", "volumeFactor"] as const) if (light[field] !== undefined) requireNumber(light[field], `lights[${index}].${field}`);
for (const field of ["useTemperature", "castsShadow"] as const) if (light[field] !== undefined) requireBoolean(light[field], `lights[${index}].${field}`);
}
for (const [index, world] of (value.worlds as unknown[]).entries()) {

View File

@@ -140,7 +140,7 @@ export type WebEngineEditCommand =
| { type: "setGreasePencilStrokes"; dataId: string; layerId: string; frame: number; baseRevision?: number; strokes: Array<{ cyclic?: boolean; materialIndex?: number; points: Array<{ position: [number, number, number]; radius?: number; opacity?: number; vertexColor?: [number, number, number, number] }> }> }
| { type: "setVertexColors"; meshId: string; attributeName: string; domain: "POINT" | "CORNER"; indices: number[]; colors: number[] }
| { type: "setVertexWeights"; objectId: string; vertexGroup: string; indices: number[]; values: number[]; normalize?: boolean; limit?: number; mirror?: boolean; mirrorAxis?: 0 | 1 | 2; mirrorTolerance?: number }
| { type: "setLightProperties"; dataId: string; properties: { type?: "POINT" | "SUN" | "SPOT" | "AREA"; areaShape?: 0 | 1 | 4 | 5; color?: [number, number, number]; energy?: number; exposure?: number; temperature?: number; useTemperature?: boolean; castsShadow?: boolean; radius?: number; spotAngle?: number; spotBlend?: number; areaSize?: number; areaSizeY?: number; areaSpread?: number; sunAngle?: number } }
| { type: "setLightProperties"; dataId: string; properties: { type?: "POINT" | "SUN" | "SPOT" | "AREA"; areaShape?: 0 | 1 | 4 | 5; diffuseFactor?: number; specularFactor?: number; transmissionFactor?: number; volumeFactor?: number; color?: [number, number, number]; energy?: number; exposure?: number; temperature?: number; useTemperature?: boolean; castsShadow?: boolean; radius?: number; spotAngle?: number; spotBlend?: number; areaSize?: number; areaSizeY?: number; areaSpread?: number; sunAngle?: number } }
| { type: "setCameraProperties"; dataId: string; properties: { projection?: "PERSPECTIVE" | "ORTHOGRAPHIC"; lensMm?: number; sensorWidthMm?: number; sensorHeightMm?: number; sensorFit?: 0 | 1 | 2; shift?: [number, number]; near?: number; far?: number; orthoScale?: number; depthOfField?: { enabled?: boolean; focusDistance?: number; apertureFStop?: number; apertureBlades?: number; apertureRotation?: number; apertureRatio?: number } } }
| { type: "setWorldProperties"; dataId: string; properties: { color?: [number, number, number]; exposure?: number; mist?: { enabled?: boolean; type?: "QUADRATIC" | "LINEAR" | "INVERSE_QUADRATIC"; start?: number; depth?: number; intensity?: number; height?: number } } }
| { type: "setSceneProperties"; dataId: string; properties: { renderEngine?: string; viewTransform?: string; look?: string; exposure?: number; gamma?: number } }