Skip to content

Feat/light probe gizmos#761

Open
bofeng-song wants to merge 11 commits into
cocos:mainfrom
bofeng-song:feat/light-probe-gizmos
Open

Feat/light probe gizmos#761
bofeng-song wants to merge 11 commits into
cocos:mainfrom
bofeng-song:feat/light-probe-gizmos

Conversation

@bofeng-song

@bofeng-song bofeng-song commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Two related scene-editor improvements.

Light probe / reflection probe gizmos

  • LightProbeGroup (select gizmo): draws all probes, the inner tetrahedron wireframe, and an editable generation bounding box with per-face min/max editing.
  • Mesh renderers using light probes: when selected, highlights the influencing tetrahedron with SH-lit probe spheres and connecting edges, using the engine's model tetrahedron index and the internal/editor/light-probe-visualization material.
  • ReflectionProbe (select gizmo): draws the influence bounding box.
  • Adds setMeshSHCoefficients to the gizmo engine utils and registers the new component gizmos.

Editor camera view persistence + focus

  • Stores the editor camera view per scene (position, rotation, view center, plus 2D content rect and scale) and restores it when the scene reopens.
  • Adds empty defaults for camera-infos / camera-uuids so reading them on first open no longer throws.
  • Improves focus range calculation for mesh, skinned mesh, particle and light probe group nodes.

Verify

中文: 打开 sponza 场景,选中 Sphere-001(已开启"使用光照探针"),应显示影响它的光照探针四面体(薄荷绿受光小球 +
连线)。

EN: Open the sponza scene and select Sphere-001 (with "Use Light Probe" enabled); the light probe tetrahedron
influencing it should be displayed (mint-green shaded probe spheres + connecting lines).

Add select gizmos for light probe and reflection probe components in the
scene editor:

- LightProbeGroup: draw all probes, the inner tetrahedron wireframe, and
  an editable generation bounding box (per-face min/max editing).
- Mesh renderers that use light probes: highlight the influencing
  tetrahedron with SH-lit probe spheres and connecting edges, driven by
  the engine's model tetrahedron index.
- ReflectionProbe: draw the influence bounding box.

Add setMeshSHCoefficients to the gizmo engine utils for the
light-probe-visualization material, and register the new component gizmos.
- Store the editor camera view per scene (position, rotation, view center,
  plus 2D content rect and scale) and restore it when the scene reopens.
- Provide empty defaults for camera-infos/camera-uuids so reading them on
  first open no longer fails.
- Improve focus range calculation for mesh, skinned mesh, particle and
  light probe group nodes.
@bofeng-song
bofeng-song requested a review from knoxHuang July 16, 2026 09:18
@@ -178,15 +190,16 @@ export class CameraService extends BaseService<ICameraEvents> implements ICamera
if (config.fov !== undefined) this.setCameraProperty({ fov: config.fov }, false);
if (config.far !== undefined) {
this._controller3D.far = config.far;

@knoxHuang knoxHuang Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Apply near/far values to the actual Camera

This only updated the controller _near/_far fields, while the CameraControllerBase setter only updates the field and does not update this._camera. Because initFromConfig() runs after the controller is active, the configured near/far values would not reach the render camera (the new 2D far2D/near2D values had the same issue) until the controller was reactivated. Please keep the _camera.near/far assignment or route this through setCameraProperty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. _applyConfig now also assigns near/far (and near2D/far2D) to the active _camera, not just the controller fields, guarded by the current dimension — restored the removed _camera.near/far assignment and added the 2D equivalents.


try {
const rpc = Rpc.getInstance();
const cameraInfos = await rpc.request('sceneConfigInstance', 'get', ['camera-infos']);

@knoxHuang knoxHuang Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Reload camera infos for every opened scene

initFromConfig() was only called from the if (!this._camera) branch (the original line 103), but _cameraInfos is stored in sceneConfigInstance by scene UUID. When CameraService is reused, opening a second scene would skip this load and keep the first scene _cameraInfos, so the camera view could be restored incorrectly or not restored at all. Please load camera-infos/camera-uuids on every onEditorOpened, or reload them for the current scene UUID.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Extracted loadCameraInfos() and call it on every onEditorOpened (awaited before defaultFocus), so a reused CameraService reloads camera-infos/camera-uuids per scene open instead of keeping the first scene's data.

let range = 0;
if (component.shapeModule?.enable) {
const shapeModule = component.shapeModule;
const ShapeType = (cc as any).ShapeType;

@knoxHuang knoxHuang Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Use the correct particle shape enum

The engine defines ParticleShapeType (packages/engine/cocos/particle/enum.ts) and does not expose cc.ShapeType. At runtime this therefore resolved to undefined, so the entire switch was skipped and particle focus ranges did not reflect the emitter size. Please use the available enum or the corresponding constants.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. cc.ShapeType does not exist and ParticleShapeType is not exported from cc, so the switch was dead. Replaced it with a local ShapeType enum (Box/Circle/Cone/Sphere/Hemisphere), matching how Creator does it in utils/node.ts.

- _applyConfig now also applies near/far (and 2D near2D/far2D) to the
  active editor camera, not only the controller fields, so scene config
  takes effect on the render camera immediately after open.
- Reload camera-infos/camera-uuids on every scene open (moved out of the
  first-camera-init path) so a reused CameraService restores the correct
  per-scene view instead of keeping the first scene's data.
- Fix particle focus-range: cc.ShapeType does not exist, which left the
  switch dead and the range at its default. Use a local ShapeType enum
  (matching Creator's utils/node.ts) instead.
@bofeng-song
bofeng-song force-pushed the feat/light-probe-gizmos branch from a25fe29 to d228848 Compare July 16, 2026 10:07
this._controller2D.lineColor = new Color(r, g, b, 255);
(this._controller3D as any).lineColor = new Color(r, g, b, 50);
this._controller2D.updateGrid();
if (!color || color.length < 4) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve support for RGB grid colors

The public method accepts number[], and the previous implementation accepted a three-component [r, g, b] value by defaulting alpha. This early return now silently ignores any RGB-only caller or older config, so the grid color is not applied. Please accept at least three components and default a to 255, or narrow the public contract and migrate existing callers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The guard now only returns for arrays shorter than 3; a 3-component RGB value is accepted with alpha defaulting to 255, so RGB-only callers and older configs still apply.

if (comp.maxPos && comp.minPos) {
const probesSize = new Vec3();
Vec3.subtract(probesSize, comp.maxPos, comp.minPos);
compRange = Math.max(Math.abs(probesSize.x / 2), Math.abs(probesSize.y / 2), Math.abs(probesSize.z / 2));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Account for the node world scale when calculating LightProbeGroup focus range

minPos/maxPos are local-space bounds, but this value is used as a world-space focus radius and no node transform is applied here. A scaled LightProbeGroup will therefore be focused too closely and may be clipped. Please multiply by the absolute world scale (or derive the range from a world-space bound), consistent with the mesh path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The LightProbeGroup range now multiplies the local minPos/maxPos half-extents by the node world scale, so a scaled group is framed with the correct world-space radius, consistent with the mesh path.

- setGridColor: accept 3-component RGB values (default alpha to 255)
  instead of ignoring any color array shorter than 4, so RGB-only callers
  and older configs still apply.
- LightProbeGroup focus range: multiply the local minPos/maxPos
  half-extents by the node world scale, so a scaled group is framed with
  the correct world-space radius (consistent with the mesh path).
break;
}
case 'cc.ParticleSystem':
compRange = getRangeFromParticleComp(component);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Convert particle bounds to world space

getRangeFromParticleComp() returns emitter dimensions in the component local space, but maxRange is used to compute a world-space camera distance. Unlike the mesh and LightProbeGroup paths, this branch ignores node.getWorldScale(). A scaled ParticleSystem will therefore still be focused too closely. Please apply the absolute world scale (and account for rotation if needed) before assigning compRange.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The ParticleSystem branch now multiplies the local-space range from getRangeFromParticleComp by the node absolute world scale (max of |ws.x|,|ws.y|,|ws.z|), consistent with the mesh and LightProbeGroup paths, so a scaled ParticleSystem is framed correctly.

getRangeFromParticleComp returns emitter dimensions in the component's
local space, but the value is used as a world-space focus radius. Like
the mesh and LightProbeGroup paths, multiply by the node's absolute
world scale so a scaled ParticleSystem is framed correctly.
case ShapeType.Circle:
case ShapeType.Sphere:
case ShapeType.Hemisphere:
range = shapeModule.radius;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Include ShapeModule scale in the particle range

ShapeModule.scale is applied by the engine to every emitter shape through its transform matrix, but this helper only uses it for Box. Sphere, hemisphere, circle, and cone ranges ignore a non-unit shapeModule.scale, so a scaled emitter can still be focused too closely even after the node world-scale fix. Please apply the absolute shape scale to these branches as well (using a conservative maximum axis scale if appropriate).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. getRangeFromParticleComp now applies shapeModule.scale to sphere/circle/hemisphere/cone as well (multiplying radius/length by the max absolute axis scale), not only Box, so a scaled emitter is framed correctly.

this._controller2D.near = config.near2D;
if (this._camera && this.is2D) this._camera.near = config.near2D;
}
if (config.wheelSpeed2D !== undefined) this._controller2D.wheelSpeed = config.wheelSpeed2D;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Persist the new 2D camera settings

_applyConfig() accepts far2D, near2D, and wheelSpeed2D, but queryConfig() still returns only the 3D far/near/wheelSpeed values. Since _saveConfig() serializes queryConfig(), calls such as updateConfig({ far2D: ..., near2D: ..., wheelSpeed2D: ... }) and the 2D reset values are lost on the next reload. Please include all three 2D fields in queryConfig() and keep the 3D values separate from the active camera when needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. queryConfig() now returns far2D/near2D/wheelSpeed2D (read from the 2D controller) and reads the 3D near/far from _controller3D instead of the active camera, so 2D settings survive save/reload.

dot = ControllerUtils.sphere(Vec3.ZERO, PROBE_SPHERE_BASE_RADIUS, PROBE_COLOR, { depthTestForTriangles: true });
this._reuseMesh = getModel(dot)?.mesh;
} else {
dot = ControllerUtils.sphere(Vec3.ZERO, PROBE_SPHERE_BASE_RADIUS, PROBE_COLOR, { depthTestForTriangles: true }, undefined);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Actually reuse the probe sphere mesh

_reuseMesh is only used as a boolean here. The second branch calls ControllerUtils.sphere(...) again, which creates a new mesh; its optional fifth argument is a material, not a mesh. A 4096-probe group therefore allocates a separate sphere mesh for every probe, defeating the purpose of MAX_PROBE_DOTS. Please use addMeshToNode(node, _reuseMesh, opts) or provide a sphere helper that reuses the mesh.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Probe dots now reuse the first sphere mesh via addMeshToNode(node, _reuseMesh, opts) (+ setMeshColor) instead of calling ControllerUtils.sphere again, so a large group no longer allocates a mesh per probe.

if (!showProbe) return;

const probes = this.target.probes;
if (!force && probes === this._probesRef) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Invalidate the dot cache when global probe display settings change

The cache checks only probes === _probesRef, but sphere size depends on lightProbeSphereVolume; showProbe, showWireframe, and the tetrahedron data also live on scene.globals.lightProbeInfo and can change without replacing target.probes. The selected gizmo can therefore keep stale size, visibility, or geometry until it is reselected. Include the relevant global settings/data in the cache key and refresh on their changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The dot cache is now keyed on both the probe array and lightProbeSphereVolume, and an onUpdate signature (volume, showProbe, showWireframe, tetrahedron/probe counts) refreshes the gizmo when those global settings/data change without reselecting.

this.updateControllerData();
}

onUpdate() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Avoid rebuilding SH geometry every frame

onUpdate calls updateControllerData() every frame; _tetraHelper.update() then recomputes SH, writes seven material uniforms per sphere, and calls drawLines to rebuild the dynamic mesh even when the model tetrahedron index, probe data, coefficients, and display scale are unchanged. Selecting a mesh therefore incurs continuous allocations and GPU updates. Cache a signature and update only when the tetrahedron/data/coefficients or required transform changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. LightProbeTetraHelper.update() now builds a signature (tetrahedron index, volume, vertex positions, SH first coefficient) and returns early when unchanged, so the SH uniform writes and drawLines rebuild no longer run every frame. mesh-renderer onUpdate also only refreshes the tetrahedron now, not the whole box.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: to fully match Creator, removed the per-frame onUpdate from the MeshRenderer gizmo so the influencing tetrahedron is now purely event-driven (show/target/node changes), giving zero per-frame cost — Creator's mesh-renderer gizmo has no onUpdate either. SkinnedMeshRenderer keeps onUpdate for animated bounds (same as Creator), where the signature cache still prevents redundant SH/line rebuilds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also wired light-probe change refresh so the event-driven mesh tetrahedron stays correct: the LIGHT_PROBE_CHANGED node event (emitted on the LightProbeGroup node when it regenerates) is now fanned out to the selected MeshRenderer/SkinnedMeshRenderer gizmos via onLightProbeChanged(), so their influencing tetrahedron refreshes without reselecting. The signature cache makes the repeated notifications cheap.

- queryConfig() now returns far2D/near2D/wheelSpeed2D and reads the 3D
  near/far from the 3D controller (not the active camera), so 2D camera
  settings persist across save/reload.
- LightProbeGroup probe dots now reuse the sphere mesh via addMeshToNode
  instead of allocating a new mesh per probe, and the dot cache is keyed
  on the probe array and lightProbeSphereVolume; an onUpdate signature
  (volume/showProbe/showWireframe/data) refreshes on global changes.
- LightProbeTetraHelper caches a signature (tetrahedron index, volume,
  vertex positions, SH coefficients) and skips the SH uniform writes and
  dynamic-mesh rebuild when unchanged. mesh-renderer onUpdate now only
  refreshes the tetrahedron instead of rebuilding the whole box each frame.
Remove the per-frame onUpdate from the MeshRenderer gizmo; the
influencing tetrahedron now refreshes only on show/target/node changes,
matching Creator's mesh-renderer gizmo (which has no onUpdate) for zero
per-frame cost. SkinnedMeshRenderer keeps onUpdate for animated bounds
(as Creator does), and the tetra helper's signature cache still avoids
redundant SH/line rebuilds there.
When a LightProbeGroup regenerates, the engine emits LIGHT_PROBE_CHANGED
on the group node, which cli surfaces as a node change. Fan that event
out to the selected MeshRenderer/SkinnedMeshRenderer gizmos so their
influencing-tetrahedron highlight refreshes even when the mesh node
itself did not move (previously it only updated on reselect). The tetra
helper's signature cache keeps repeated notifications cheap.
info ? (info.lightProbeSphereVolume ?? 1) : 1,
info ? (info.showProbe ?? true) : true,
info ? (info.showWireframe ?? true) : true,
data?.tetrahedrons?.length ?? 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Include probe data contents in the LightProbeGroup refresh signature

_computeInfoSig() only uses probe/tetrahedron counts. Regenerating or rebaking probes with the same counts leaves this signature unchanged, so onUpdate() skips updateControllerData() and the selected gizmo keeps stale probe positions and wireframe edges. Please include the relevant positions/indices or invalidate the signature from the light-probe-changed event.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The LightProbeGroup gizmo now implements onLightProbeChanged() which clears its dot/info caches and re-runs updateControllerData, so regenerate/rebake with unchanged probe/tetrahedron counts still refreshes positions and wireframe instead of being skipped by the count-only signature.

if (isInner) indices.push(tet.vertex3);

// 签名:四面体索引 + 体积 + 各顶点位置/SH 首系数。未变化时跳过昂贵的 SH 与连线重建。
let sig = `${tetIndex}|${volume}|${isInner ? 1 : 0}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Include all SH inputs in the tetrahedron cache signature

The signature includes only the first SH coefficient for each vertex. It omits reduceRinging, coefficients 1 through 8, and the vertex normals used to draw outer-cell edges. A rebake or settings change that preserves positions and coefficient 0 will therefore be ignored and the visualization will remain stale. Please include all render inputs or invalidate the cache on every light-probe data/settings change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added reduceRinging to the signature and an invalidate() on the helper that is called on light-probe changes (via onLightProbeChanged), forcing a full rebuild so all SH coefficients, vertex normals and settings are picked up rather than only coefficient 0.

}

// 探针数据变化(探针组重生成等)时刷新影响四面体;本节点未移动也需响应
onLightProbeChanged() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep a trigger for MeshRenderer tetrahedron-index changes

Before this removal, onUpdate() was the only path that observed model.tetrahedronIndex changes made by the engine update loop. The new onLightProbeChanged() path covers global probe-data changes, but it does not run when a selected MeshRenderer moves or animates across tetrahedra without a node-change event. The helper can therefore remain cached on the old tetrahedron. Please retain a cheap per-frame index check or emit a model change event from the engine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Re-added a minimal per-frame check in the MeshRenderer gizmo onUpdate that compares model.tetrahedronIndex and only refreshes when it changes, so movement/animation across tetrahedra (updated by the engine loop without a node event) is captured without rebuilding SH/lines every frame.

- Particle focus range now applies shapeModule.scale to sphere/circle/
  hemisphere/cone emitters too (not only Box), via the max absolute axis
  scale.
- Re-add a cheap per-frame tetrahedron-index check to the MeshRenderer
  gizmo so movement/animation across tetrahedra (updated by the engine
  loop without a node event) is captured; it only rebuilds when the index
  changes.
- Invalidate the tetrahedron helper cache on light-probe changes (covers
  all SH coefficients, normals and settings, not just the first
  coefficient) and include reduceRinging in its signature.
- Force-refresh the LightProbeGroup gizmo on light-probe changes by
  clearing its dot/info caches, so regenerate/rebake with unchanged
  counts no longer leaves stale probe positions or wireframe.
// 引擎在渲染循环里更新 model.tetrahedronIndex(不触发节点事件),移动/动画跨四面体时需捕获。
// 仅做一次索引整数比较,变化时才刷新,避免每帧重建 SH/连线。
const idx = (this.target as any)?.model?.tetrahedronIndex ?? -1;
if (idx !== this._lastTetIndex) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Continue polling the helper signature for display-setting changes

onUpdate() now calls _tetraHelper.update() only when tetrahedronIndex changes. However lightProbeSphereVolume and reduceRinging can change while the index stays the same, and their LightProbeInfo setters do not emit LIGHT_PROBE_CHANGED. The helper signature already includes these values, but it is never evaluated, so a selected MeshRenderer keeps stale sphere size or SH shading. Call update() each frame and rely on its cache, or track these settings in this fast path too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. MeshRenderer onUpdate now calls _tetraHelper.update() every frame and relies on its signature cache (which already includes tetrahedronIndex, volume and reduceRinging), instead of gating on tetrahedronIndex alone. So volume/reduceRinging changes that keep the index are now picked up; the cache still short-circuits when nothing changed.

if (!node) return;
// 光照探针数据变化(如探针组重新生成)时,探针组自身节点会收到该事件,
// 但受其影响的 mesh 的四面体高亮挂在别的节点上,需主动通知选中的探针消费者刷新。
if (opts?.type === NodeEventType.LIGHT_PROBE_CHANGED) {

@knoxHuang knoxHuang Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Handle the engine baking-change event as well

The engine emits NodeEventType.LIGHT_PROBE_BAKING_CHANGED from LightProbeInfo.onProbeBakeFinished() and onProbeBakeCleared(), not LIGHT_PROBE_CHANGED. The Node service currently forwards only the latter, so this branch does not invalidate selected gizmos after a bake. A selected gizmo can show stale SH colors and tetrahedron data until it is refreshed or reselected. Please forward and handle LIGHT_PROBE_BAKING_CHANGED too (or invalidate directly from the bake event).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The Node service now forwards LIGHT_PROBE_BAKING_CHANGED (emitted by LightProbeInfo on bake finish/clear) as a node change, and the gizmo service handles it the same as LIGHT_PROBE_CHANGED (invalidating the tetra caches). Since the engine emits the bake event on every node in the scene, the refresh is debounced via a microtask so it runs once per bake.

- Forward the engine's LIGHT_PROBE_BAKING_CHANGED node event (emitted by
  LightProbeInfo on bake finish/clear) from the Node service, and handle
  it in the gizmo service alongside LIGHT_PROBE_CHANGED. The refresh is
  debounced via a microtask because the engine emits the bake event on
  every node in the scene.
- MeshRenderer gizmo onUpdate now calls the tetra helper every frame and
  relies on its signature cache, instead of gating on tetrahedronIndex
  only. This catches lightProbeSphereVolume / reduceRinging changes,
  whose setters do not emit LIGHT_PROBE_CHANGED and leave the index
  unchanged.
@bofeng-song bofeng-song reopened this Jul 17, 2026

@knoxHuang knoxHuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review approved. The latest fixes address the reported light-probe event and cache-refresh issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants