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 { import {
getGenerationCreditCostFromImageGenConfig, getGenerationCreditCostFromImageGenConfig,
getImageCreditCostFromConfig, getImageCreditCostFromConfig,
getImageModelCreditDiscountLabel,
} from "@/lib/ai/image-model-pricing"; } from "@/lib/ai/image-model-pricing";
import { getVideoToolOptionsForMode } from "@/lib/ai/video-tool-capability"; import { getVideoToolOptionsForMode } from "@/lib/ai/video-tool-capability";
import type { Attachment, ChatMessage } from "@/lib/types"; 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"> <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 <SelectItem
key={model.id} key={model.id}
value={model.id} value={model.id}
@ -9368,19 +9374,34 @@ export function CanvasChatPanel({
</span> </span>
<span className="truncate text-[12px] font-medium leading-none">{model.displayName}</span> <span className="truncate text-[12px] font-medium leading-none">{model.displayName}</span>
</span> </span>
{modelSelectValue === model.id ? ( <span className="flex shrink-0 items-center gap-1.5">
<span {discountLabel ? (
className={cn( <span
"text-sm font-semibold", className={cn(
isVideoWorkspace ? "text-foreground" : "text-[#1f2937]" "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"
</span> )}
) : null} >
{discountLabel}
</span>
) : null}
{modelSelectValue === model.id ? (
<span
className={cn(
"text-sm font-semibold",
isVideoWorkspace ? "text-foreground" : "text-[#1f2937]"
)}
>
✓
</span>
) : null}
</span>
</span> </span>
</SelectItem> </SelectItem>
))} );
})}
{selectableModels.length === 0 ? ( {selectableModels.length === 0 ? (
<div <div
className={cn( 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 React, { Suspense, useEffect, useMemo, useRef } from "react";
import * as THREE from "three"; import * as THREE from "three";
import type { Mesh, Object3D } from "three"; import type { Mesh, Object3D } from "three";
import { KTX2Loader } from "three-stdlib";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { buildThreeAssetUrl } from "@/lib/canvas/three-asset-url"; import { buildThreeAssetUrl } from "@/lib/canvas/three-asset-url";
@ -72,6 +73,8 @@ const CAMERA_PRESET_POSITIONS: Record<
top: [0, 3.4, 0.001], top: [0, 3.4, 0.001],
}; };
const KTX2_TRANSCODER_PATH = "/three/basis/";
function CameraPresetController({ function CameraPresetController({
controlsRef, controlsRef,
preset, preset,
@ -201,7 +204,28 @@ function ModelScene({
onLayersChange?: (layers: Model3DLayer[]) => void; onLayersChange?: (layers: Model3DLayer[]) => void;
onStatsChange?: (stats: Model3DStats) => 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 scene = useMemo(() => model.scene.clone(), [model.scene]);
const meta = useMemo(() => collectSceneMeta(scene), [scene]); const meta = useMemo(() => collectSceneMeta(scene), [scene]);
@ -219,15 +243,25 @@ function ModelScene({
useEffect(() => { useEffect(() => {
for (const { mesh } of meta.meshRefs) { for (const { mesh } of meta.meshRefs) {
mesh.castShadow = true;
mesh.receiveShadow = true;
const materials = Array.isArray(mesh.material) const materials = Array.isArray(mesh.material)
? mesh.material ? mesh.material
: [mesh.material]; : [mesh.material];
for (const material of materials) { for (const material of materials) {
if (!material) continue; 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) { if ("wireframe" in material) {
(material as any).wireframe = settings.wireframe; (material as any).wireframe = settings.wireframe;
material.needsUpdate = true;
} }
material.needsUpdate = true;
} }
} }
}, [meta.meshRefs, settings.wireframe]); }, [meta.meshRefs, settings.wireframe]);
@ -381,6 +415,11 @@ export function Model3DViewer({
}} }}
className="bg-gradient-to-b from-zinc-900 to-zinc-950" className="bg-gradient-to-b from-zinc-900 to-zinc-950"
gl={{ preserveDrawingBuffer: true }} gl={{ preserveDrawingBuffer: true }}
onCreated={({ gl }) => {
gl.outputColorSpace = THREE.SRGBColorSpace;
gl.toneMapping = THREE.ACESFilmicToneMapping;
gl.toneMappingExposure = 1;
}}
> >
<ViewerCaptureBridge onCaptureReady={onCaptureReady} /> <ViewerCaptureBridge onCaptureReady={onCaptureReady} />
<CameraPresetController <CameraPresetController

File diff suppressed because one or more lines are too long

Binary file not shown.