fix: restore 3d pbr textures in canvas viewer
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-14 10:48:39 +08:00
parent 86b1ee9eeb
commit 89ec01cfb8
4 changed files with 93 additions and 14 deletions

View File

@ -72,6 +72,7 @@ import {
import {
getGenerationCreditCostFromImageGenConfig,
getImageCreditCostFromConfig,
getImageModelCreditDiscountLabel,
} from "@/lib/ai/image-model-pricing";
import { getVideoToolOptionsForMode } from "@/lib/ai/video-tool-capability";
import type { Attachment, ChatMessage } from "@/lib/types";
@ -9321,7 +9322,12 @@ export function CanvasChatPanel({
)}
>
<div className="max-h-[312px] space-y-0.5 overflow-y-auto pr-1">
{selectableModels.map((model) => (
{selectableModels.map((model) => {
const discountLabel = getImageModelCreditDiscountLabel(
model.imageGenConfig
);
return (
<SelectItem
key={model.id}
value={model.id}
@ -9368,19 +9374,34 @@ export function CanvasChatPanel({
</span>
<span className="truncate text-[12px] font-medium leading-none">{model.displayName}</span>
</span>
{modelSelectValue === model.id ? (
<span
className={cn(
"text-sm font-semibold",
isVideoWorkspace ? "text-foreground" : "text-[#1f2937]"
)}
>
✓
</span>
) : null}
<span className="flex shrink-0 items-center gap-1.5">
{discountLabel ? (
<span
className={cn(
"rounded-md border px-1.5 py-0.5 text-[10px] font-semibold leading-none",
isVideoWorkspace
? "border-border/70 bg-muted/80 text-foreground/80"
: "border-[#d0d4dc] bg-[#eef1f5] text-[#5c6574] dark:border-white/15 dark:bg-white/10 dark:text-zinc-200"
)}
>
{discountLabel}
</span>
) : null}
{modelSelectValue === model.id ? (
<span
className={cn(
"text-sm font-semibold",
isVideoWorkspace ? "text-foreground" : "text-[#1f2937]"
)}
>
✓
</span>
) : null}
</span>
</span>
</SelectItem>
))}
);
})}
{selectableModels.length === 0 ? (
<div
className={cn(

View File

@ -13,6 +13,7 @@ import { AlertCircle, Box, ExternalLink, Loader2 } from "lucide-react";
import React, { Suspense, useEffect, useMemo, useRef } from "react";
import * as THREE from "three";
import type { Mesh, Object3D } from "three";
import { KTX2Loader } from "three-stdlib";
import { Button } from "@/components/ui/button";
import { buildThreeAssetUrl } from "@/lib/canvas/three-asset-url";
@ -72,6 +73,8 @@ const CAMERA_PRESET_POSITIONS: Record<
top: [0, 3.4, 0.001],
};
const KTX2_TRANSCODER_PATH = "/three/basis/";
function CameraPresetController({
controlsRef,
preset,
@ -201,7 +204,28 @@ function ModelScene({
onLayersChange?: (layers: Model3DLayer[]) => void;
onStatsChange?: (stats: Model3DStats) => void;
}) {
const model = useGLTF(url);
const { gl } = useThree();
const ktx2Loader = useMemo(() => {
const nextLoader = new KTX2Loader();
nextLoader.setTranscoderPath(KTX2_TRANSCODER_PATH);
nextLoader.detectSupport(gl);
return nextLoader;
}, [gl]);
useEffect(() => {
return () => {
ktx2Loader.dispose();
};
}, [ktx2Loader]);
const model = useGLTF(
url,
true,
true,
(loader) => {
loader.setKTX2Loader(ktx2Loader);
},
);
const scene = useMemo(() => model.scene.clone(), [model.scene]);
const meta = useMemo(() => collectSceneMeta(scene), [scene]);
@ -219,15 +243,25 @@ function ModelScene({
useEffect(() => {
for (const { mesh } of meta.meshRefs) {
mesh.castShadow = true;
mesh.receiveShadow = true;
const materials = Array.isArray(mesh.material)
? mesh.material
: [mesh.material];
for (const material of materials) {
if (!material) continue;
const textureLikeKeys = ["map", "emissiveMap"] as const;
for (const key of textureLikeKeys) {
const texture = (material as any)?.[key];
if (texture && "colorSpace" in texture) {
texture.colorSpace = THREE.SRGBColorSpace;
texture.needsUpdate = true;
}
}
if ("wireframe" in material) {
(material as any).wireframe = settings.wireframe;
material.needsUpdate = true;
}
material.needsUpdate = true;
}
}
}, [meta.meshRefs, settings.wireframe]);
@ -381,6 +415,11 @@ export function Model3DViewer({
}}
className="bg-gradient-to-b from-zinc-900 to-zinc-950"
gl={{ preserveDrawingBuffer: true }}
onCreated={({ gl }) => {
gl.outputColorSpace = THREE.SRGBColorSpace;
gl.toneMapping = THREE.ACESFilmicToneMapping;
gl.toneMappingExposure = 1;
}}
>
<ViewerCaptureBridge onCaptureReady={onCaptureReady} />
<CameraPresetController

File diff suppressed because one or more lines are too long

Binary file not shown.