fix: 优化材质金属度渲染表现
This commit is contained in:
parent
8d76d4ef9d
commit
7f3fe36da9
@ -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" />
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user