Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Removes empty-string `phase` overrides from material pipeline states.
*
* Inspector dumps encode an unset effect pass.phase as `PassStatesEditor.phase = ''`.
* `Pass.fillPipelineInfo` treats any defined phase as an override, and
* `getPhaseID('')` registers a unique unused render phase. The preview camera
* never draws that phase, so the mesh disappears after apply.
*
* @param states Material `_states` array, a single override record, or unrelated input.
* @returns Whether any empty phase was removed.
*
* @example
* ```ts
* const states = [{ phase: '', primitive: 7 }, { phase: 'forward-add' }];
* omitEmptyMaterialPhaseOverrides(states);
* // states[0] has no phase; states[1].phase is still 'forward-add'
* ```
*/
export function omitEmptyMaterialPhaseOverrides(states: unknown): boolean {
if (Array.isArray(states)) {
let mutated = false;
for (const state of states) {
if (omitEmptyPhaseFromRecord(state)) {
mutated = true;
}
}
return mutated;
}
return omitEmptyPhaseFromRecord(states);
}

function omitEmptyPhaseFromRecord(state: unknown): boolean {
if (!state || typeof state !== 'object' || Array.isArray(state)) {
return false;
}
const record = state as Record<string, unknown>;
if (record.phase !== '') {
return false;
}
delete record.phase;
return true;
}
27 changes: 27 additions & 0 deletions src/core/scene/scene-process/service/preview/material-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const transientMaterialOverridePatchKey = Symbol.for('cocos.cli.materialPreview.

import type { IMaterialPreviewInstance } from '../../../common/preview';
import { loadPreviewAsset } from './asset-reload';
import { omitEmptyMaterialPhaseOverrides } from './material-preview-states';

function collectTextureProperties(value: any, out: any[]) {
if (!value) return;
Expand Down Expand Up @@ -203,6 +204,10 @@ function applyMaterialRecord(material: any, key: '_defines' | '_states', overrid
applyAt(passIdx);
}

if (key === '_states') {
omitEmptyMaterialPhaseOverrides(records);
}

material._update?.(true);
}

Expand Down Expand Up @@ -238,6 +243,13 @@ function installTransientMaterialOverridePatch() {
};
}

/** Drops dump-default `phase: ''` and rebuilds passes so the preview camera can still draw. */
function rebuildPassesWithoutEmptyPhase(material: Material) {
if (omitEmptyMaterialPhaseOverrides((material as any)._states)) {
(material as any)._update?.(true);
}
}

export class MaterialPreview extends InteractivePreview implements IMaterialPreviewInstance {
private lightComp!: DirectionalLight;
private modelComp!: MeshRenderer;
Expand Down Expand Up @@ -308,8 +320,23 @@ export class MaterialPreview extends InteractivePreview implements IMaterialPrev
this._modelNode = this.modelComp.node;
}

/*
```mermaid
sequenceDiagram
participant Panel as MaterialPanel apply
participant Preview as MaterialPreview.setMaterial
participant Material as cc.Material
participant Pass as cc.Pass
Panel->>Preview: dump-built Material (_states.phase === "")
Preview->>Material: omit empty phase, _update
Material->>Pass: fillPipelineInfo without phase override
Pass-->>Preview: default phase (camera can draw)
Preview->>Preview: wrap MaterialInstance and assign
```
*/
public setMaterial(material: Material | null, force = false) {
if (material && (force || material !== this.material)) {
rebuildPassesWithoutEmptyPhase(material);
const comp = this.modelComp;
const _matInsInfo = {
parent: material,
Expand Down
36 changes: 36 additions & 0 deletions src/core/scene/test/material-preview-states.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { omitEmptyMaterialPhaseOverrides } from '../scene-process/service/preview/material-preview-states';

describe('omitEmptyMaterialPhaseOverrides', () => {
it('removes empty-string phase from dump states so apply uses the effect default', () => {
const states = [
{ phase: '', primitive: 7 },
{ primitive: 7 },
];
const single = { phase: '', rasterizerState: { cullMode: 2 } };

expect(omitEmptyMaterialPhaseOverrides(states)).toBe(true);
expect(states[0]).toEqual({ primitive: 7 });
expect(states[1]).toEqual({ primitive: 7 });

expect(omitEmptyMaterialPhaseOverrides(single)).toBe(true);
expect(single).toEqual({ rasterizerState: { cullMode: 2 } });
expect(omitEmptyMaterialPhaseOverrides(states)).toBe(false);
});

it('keeps named or numeric phase overrides and ignores unrelated input', () => {
const states = [
{ phase: 'forward-add', primitive: 7 },
{ phase: 'default' },
{ phase: 16 },
];

expect(omitEmptyMaterialPhaseOverrides(states)).toBe(false);
expect(states[0].phase).toBe('forward-add');
expect(states[1].phase).toBe('default');
expect(states[2].phase).toBe(16);
expect(omitEmptyMaterialPhaseOverrides(null)).toBe(false);
expect(omitEmptyMaterialPhaseOverrides(undefined)).toBe(false);
expect(omitEmptyMaterialPhaseOverrides('')).toBe(false);
expect(omitEmptyMaterialPhaseOverrides([{ phase: null }])).toBe(false);
});
});
Loading