fix: 优化材质金属度渲染表现

This commit is contained in:
WuYongJie 2026-05-13 16:35:43 +08:00
parent 8d76d4ef9d
commit 7f3fe36da9
6 changed files with 111 additions and 70 deletions

View File

@ -255,6 +255,7 @@ function FinishWorkbenchPreviewCard({ form }: { form: FinishForm }) {
const textureSize = imageUrl
? `${Math.max(72, Math.round((form.textureRepeat || 1) * 96))}%`
: '100% 100%'
const metalHighlightOpacity = Math.min(0.5, Math.max(0, form.metalness * (1 - form.roughness * 0.55)))
return (
<div className="ml-preview-surface ml-sphere-preview relative h-[180px] w-full overflow-hidden rounded-lg bg-white p-3 ring-1 ring-black/5">
@ -283,9 +284,18 @@ function FinishWorkbenchPreviewCard({ form }: { form: FinishForm }) {
backgroundSize: `100% 100%, 100% 100%, ${textureSize}`,
backgroundPosition: 'center',
opacity: Math.max(0.35, Math.min(1, form.opacity)),
filter: `saturate(${1 + form.metalness * 0.18}) contrast(${1 + form.metalness * 0.14}) brightness(${1 - form.roughness * 0.04})`,
filter: `brightness(${1 - form.roughness * 0.04})`,
}}
>
<div
className="pointer-events-none absolute inset-0 rounded-full"
style={{
opacity: metalHighlightOpacity,
background:
'linear-gradient(128deg, transparent 8%, rgba(255,255,255,0.72) 24%, transparent 39%, rgba(255,255,255,0.32) 52%, rgba(16,23,25,0.2) 70%, transparent 84%)',
mixBlendMode: 'screen',
}}
/>
<div className="pointer-events-none absolute inset-0 rounded-full shadow-[inset_-24px_-30px_54px_rgba(38,48,51,0.22),inset_18px_16px_38px_rgba(255,255,255,0.28)]" />
</div>
<div className="pointer-events-none absolute top-[48%] h-px w-[74%] max-w-[300px] bg-gradient-to-r from-transparent via-black/10 to-transparent" />

View File

@ -0,0 +1,20 @@
import * as THREE from "three";
import { normalizeTooBrightMaterial } from "@/lib/3d/engine-highlight-resources";
describe("normalizeTooBrightMaterial", () => {
it("保留材质编辑器写入的金属度、粗糙度和颜色", () => {
const material = new THREE.MeshStandardMaterial({
color: "#f2f2f2",
metalness: 0.92,
roughness: 0.12,
envMapIntensity: 1.4,
});
normalizeTooBrightMaterial(material);
expect(material.metalness).toBeCloseTo(0.92);
expect(material.roughness).toBeCloseTo(0.12);
expect(material.envMapIntensity).toBeCloseTo(1.4);
expect(material.color.getHexString()).toBe("f2f2f2");
});
});

View File

@ -0,0 +1,46 @@
import * as THREE from "three";
import { MaterialManager, type MaterialConfig } from "@/lib/3d/material-manager";
function createMaterialConfig(overrides: Partial<MaterialConfig> = {}): MaterialConfig {
return {
id: "material-metal",
name: "测试金属",
categoryId: "category-metal",
categoryName: "金属",
thumbnailUrl: null,
textureUrl: null,
normalMapUrl: null,
roughnessMapUrl: null,
metalnessMapUrl: null,
aoMapUrl: null,
color: "#c9ccd0",
metalness: 0.92,
roughness: 0.22,
opacity: 1,
textureRepeat: 1,
textureRotation: 0,
textureSizeMm: 600,
...overrides,
};
}
describe("MaterialManager", () => {
it("高金属度材质默认获得更强环境反射", () => {
const material = MaterialManager.getInstance().createImmediateMaterial(
createMaterialConfig(),
);
expect(material).toBeInstanceOf(THREE.MeshStandardMaterial);
expect(material.metalness).toBeCloseTo(0.92);
expect(material.roughness).toBeCloseTo(0.22);
expect(material.envMapIntensity).toBeCloseTo(1.65);
});
it("显式环境反射强度优先于金属默认值", () => {
const material = MaterialManager.getInstance().createImmediateMaterial(
createMaterialConfig({ envMapIntensity: 1.18 }),
);
expect(material.envMapIntensity).toBeCloseTo(1.18);
});
});

View File

@ -400,73 +400,8 @@ export function normalizeTooBrightMaterial(
emissiveIntensity?: number;
};
if (colorLikeMaterial.color instanceof THREE.Color) {
const originalColor = colorLikeMaterial.color.clone();
const maxChannel = Math.max(
originalColor.r,
originalColor.g,
originalColor.b,
);
if (maxChannel > 1) {
colorLikeMaterial.color.multiplyScalar(1 / maxChannel);
}
const r = colorLikeMaterial.color.r;
const g = colorLikeMaterial.color.g;
const b = colorLikeMaterial.color.b;
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
const chroma = Math.max(r, g, b) - Math.min(r, g, b);
const isNearNeutral = chroma < 0.22;
if (isNearNeutral && luminance > 0.38) {
const grayStrength = THREE.MathUtils.clamp((luminance - 0.38) / 0.62, 0, 1);
const hasColorMap = Boolean(colorLikeMaterial.map);
const nearWhiteGray = hasColorMap
? 0.46 - grayStrength * 0.08
: 0.4 - grayStrength * 0.1;
const grayTarget = new THREE.Color().setScalar(nearWhiteGray);
colorLikeMaterial.color.lerp(grayTarget, hasColorMap ? 0.92 : 0.98);
if (typeof colorLikeMaterial.roughness === "number") {
colorLikeMaterial.roughness = Math.max(
colorLikeMaterial.roughness,
0.95,
);
}
if (typeof colorLikeMaterial.envMapIntensity === "number") {
colorLikeMaterial.envMapIntensity = Math.min(
colorLikeMaterial.envMapIntensity,
0.03,
);
}
if (typeof colorLikeMaterial.metalness === "number") {
colorLikeMaterial.metalness = Math.min(colorLikeMaterial.metalness, 0.02);
}
if (
colorLikeMaterial.emissive instanceof THREE.Color &&
typeof colorLikeMaterial.emissiveIntensity === "number"
) {
colorLikeMaterial.emissive.setScalar(0);
colorLikeMaterial.emissiveIntensity = Math.min(
colorLikeMaterial.emissiveIntensity,
0.01,
);
}
}
}
if (typeof colorLikeMaterial.roughness === "number") {
colorLikeMaterial.roughness = THREE.MathUtils.clamp(
colorLikeMaterial.roughness,
0.25,
1,
);
}
if (typeof colorLikeMaterial.metalness === "number") {
colorLikeMaterial.metalness = THREE.MathUtils.clamp(
colorLikeMaterial.metalness,
0,
0.5,
);
}
// 材质编辑器的物理参数要在 3D 世界里按设计师填写值生效。
// 这里不再归一化 color / roughness / metalness / envMapIntensity。
if (colorLikeMaterial.map) {
colorLikeMaterial.map.colorSpace = THREE.SRGBColorSpace;

View File

@ -42,6 +42,36 @@ export interface MaterialCategoryWithMaterials {
type RuntimeMaterial = THREE.MeshStandardMaterial | THREE.MeshPhysicalMaterial
function isMetalLikeMaterial(config: MaterialConfig): boolean {
const text = `${config.categoryName ?? ''} ${config.name}`.toLowerCase()
return (
config.metalness >= 0.65 ||
text.includes('金属') ||
text.includes('不锈钢') ||
text.includes('钢') ||
text.includes('铝') ||
text.includes('铜') ||
text.includes('铁') ||
text.includes('metal') ||
text.includes('steel') ||
text.includes('aluminum') ||
text.includes('brass')
)
}
function resolveEnvMapIntensity(config: MaterialConfig, options: { isGlassLikeMaterial: boolean }): number {
if (typeof config.envMapIntensity === 'number') {
return config.envMapIntensity
}
if (options.isGlassLikeMaterial) {
return 1.2
}
if (isMetalLikeMaterial(config)) {
return 1.65
}
return 1
}
export class MaterialManager {
private static instance: MaterialManager | null = null
private materialCache: Map<string, RuntimeMaterial> = new Map()
@ -135,7 +165,7 @@ export class MaterialManager {
depthWrite: safeOpacity >= 1,
})
material.envMapIntensity = resolvedConfig.envMapIntensity ?? (isGlassLikeMaterial ? 1.2 : 1)
material.envMapIntensity = resolveEnvMapIntensity(resolvedConfig, { isGlassLikeMaterial })
material.userData = {
...material.userData,
textureSizeMm: resolvedConfig.textureSizeMm ?? 600,

View File

@ -164,7 +164,7 @@ const TEXTURE_LIBRARY = {
const CATEGORY_DEFAULT_PRESETS: Record<MaterialFamily, MaterialPBRPreset> = {
wood: { metalness: 0, roughness: 0.62, opacity: 1, textureRepeat: 1.8, envMapIntensity: 0.55 },
solid: { metalness: 0, roughness: 0.42, opacity: 1, textureRepeat: 1, envMapIntensity: 0.4 },
metal: { metalness: 1, roughness: 0.28, opacity: 1, textureRepeat: 1.3, envMapIntensity: 1.25 },
metal: { metalness: 1, roughness: 0.28, opacity: 1, textureRepeat: 1.3, envMapIntensity: 1.65 },
glass: {
metalness: 0,
roughness: 0.08,