diff --git a/packages/polycss/src/api/createPolyScene.test.ts b/packages/polycss/src/api/createPolyScene.test.ts index 2237f19c..45169a97 100644 --- a/packages/polycss/src/api/createPolyScene.test.ts +++ b/packages/polycss/src/api/createPolyScene.test.ts @@ -2368,6 +2368,30 @@ describe("createPolyScene", () => { expect(initialSvg.querySelector("path")?.getAttribute("d")).toBe(initialPathD); }); + it("baked preview can skip shadow rewrites", () => { + scene = makeScene(host, { + textureLighting: "baked", + directionalLight: { direction: [0, 0, 1] }, + }); + scene.add(makeParseResult([triangle()]), { castShadow: true }); + const initialSvg = host.querySelector(".polycss-shadow") as SVGSVGElement; + const initialTransform = initialSvg.style.transform; + const initialPathD = initialSvg.querySelector("path")?.getAttribute("d"); + const previewScene = scene as PolySceneHandle & { + previewBakedSolidLighting(next: Pick & { + skipShadows?: boolean; + }): boolean; + }; + + expect(previewScene.previewBakedSolidLighting({ + directionalLight: { direction: [1, 0, 1] }, + skipShadows: true, + })).toBe(true); + + expect(initialSvg.style.transform).toBe(initialTransform); + expect(initialSvg.querySelector("path")?.getAttribute("d")).toBe(initialPathD); + }); + it("non-shadow helper movement does not overwrite baked preview shadows", () => { scene = makeScene(host, { textureLighting: "baked", diff --git a/packages/polycss/src/api/createPolyScene.ts b/packages/polycss/src/api/createPolyScene.ts index aceb9d4a..fcba80eb 100644 --- a/packages/polycss/src/api/createPolyScene.ts +++ b/packages/polycss/src/api/createPolyScene.ts @@ -1548,11 +1548,15 @@ export function createPolyScene( } function previewBakedSolidLighting( - next: Pick, "directionalLight" | "ambientLight">, + next: Pick, "directionalLight" | "ambientLight"> & { + skipShadows?: boolean; + }, ): boolean { if ((currentOptions.textureLighting ?? "baked") !== "baked") return false; applyLightingVars(sceneEl, { ...currentOptions, ...next }); - if (next.directionalLight?.direction) emitSceneShadows(next.directionalLight.direction as Vec3); + if (!next.skipShadows && next.directionalLight?.direction) { + emitSceneShadows(next.directionalLight.direction as Vec3); + } let installed = false; for (const entry of meshes) { applyPreviewMeshLightVars(entry, next); diff --git a/website/src/components/GalleryWorkbench/GalleryWorkbench.tsx b/website/src/components/GalleryWorkbench/GalleryWorkbench.tsx index 023bc235..cb88ac3b 100644 --- a/website/src/components/GalleryWorkbench/GalleryWorkbench.tsx +++ b/website/src/components/GalleryWorkbench/GalleryWorkbench.tsx @@ -166,6 +166,8 @@ const RESPONSIVE_ZOOM_BOTTOM_RESERVE = 72; const RESPONSIVE_ZOOM_MIN_SCALE = 0.42; const RESPONSIVE_SHADOW_EXTEND_BASE = 3200; const RESPONSIVE_SHADOW_EXTEND_MIN = 2000; +const RESPONSIVE_SHADOW_PREVIEW_EXTEND_BASE = 1800; +const RESPONSIVE_SHADOW_PREVIEW_EXTEND_MIN = 800; function clamp(value: number, min: number, max: number): number { if (!Number.isFinite(value)) return min; @@ -186,15 +188,38 @@ function responsiveZoomScaleForViewport(width: number, height: number): number { return clamp(Math.min(widthScale, heightScale), RESPONSIVE_ZOOM_MIN_SCALE, 1); } -function responsiveShadowMaxExtend(value: number, viewportScale: number): number { +function responsiveCappedShadowMaxExtend( + value: number, + viewportScale: number, + base: number, + min: number, +): number { if (viewportScale >= 0.995) return value; const cap = Math.max( - RESPONSIVE_SHADOW_EXTEND_MIN, - Math.round(RESPONSIVE_SHADOW_EXTEND_BASE * viewportScale), + min, + Math.round(base * viewportScale), ); return Math.min(value, cap); } +function responsiveShadowMaxExtend(value: number, viewportScale: number): number { + return responsiveCappedShadowMaxExtend( + value, + viewportScale, + RESPONSIVE_SHADOW_EXTEND_BASE, + RESPONSIVE_SHADOW_EXTEND_MIN, + ); +} + +function responsiveShadowPreviewMaxExtend(value: number, viewportScale: number): number { + return responsiveCappedShadowMaxExtend( + value, + viewportScale, + RESPONSIVE_SHADOW_PREVIEW_EXTEND_BASE, + RESPONSIVE_SHADOW_PREVIEW_EXTEND_MIN, + ); +} + function initialResponsiveZoomScale(): number { if (typeof window === "undefined") return 1; return responsiveZoomScaleForViewport(window.innerWidth, window.innerHeight); @@ -805,17 +830,21 @@ export default function GalleryWorkbench() { markSceneRouteDirty(); setSceneOptions((current) => ({ ...current, ...partial })); }, [markSceneRouteDirty]); + const responsiveZoomScale = useResponsiveViewportZoomScale(viewportRef); const canPreviewSceneOptions = useCallback( (options: SceneOptionsState) => options.renderer === "vanilla" && transientSceneHandleRef.current !== null, [], ); const previewSceneOptions = useCallback((options: SceneOptionsState) => { - transientSceneHandleRef.current?.applyLightOptions(options); - }, []); + const previewShadow = responsiveZoomScale >= 0.995 || options.textureLighting === "dynamic"; + transientSceneHandleRef.current?.applyLightOptions({ + ...options, + shadowMaxExtend: responsiveShadowPreviewMaxExtend(options.shadowMaxExtend, responsiveZoomScale), + }, { shadow: previewShadow }); + }, [responsiveZoomScale]); const { handleCameraChange } = useGuiCameraSync({ setSceneOptions }); - const responsiveZoomScale = useResponsiveViewportZoomScale(viewportRef); const renderSceneOptions = useMemo(() => { const shadowMaxExtend = responsiveShadowMaxExtend(sceneOptions.shadowMaxExtend, responsiveZoomScale); if (responsiveZoomScale === 1 && shadowMaxExtend === sceneOptions.shadowMaxExtend) return sceneOptions; diff --git a/website/src/components/VanillaScene/VanillaScene.tsx b/website/src/components/VanillaScene/VanillaScene.tsx index 59f05909..c495bdde 100644 --- a/website/src/components/VanillaScene/VanillaScene.tsx +++ b/website/src/components/VanillaScene/VanillaScene.tsx @@ -283,13 +283,14 @@ function bakedLightingSignature( export interface VanillaSceneTransientHandle { applySceneOptions(options: SceneOptionsState): void; - applyLightOptions(options: SceneOptionsState): void; + applyLightOptions(options: SceneOptionsState, preview?: { shadow?: boolean }): void; } type BakedSolidLightingPreviewSceneHandle = PolySceneHandle & { previewBakedSolidLighting?: (next: { directionalLight?: PolyDirectionalLight; ambientLight?: PolyAmbientLight; + skipShadows?: boolean; }) => boolean; commitBakedSolidLighting?: () => boolean; }; @@ -411,16 +412,23 @@ export function VanillaScene({ } }, []); - const applyTransientLightOptions = useCallback((nextOptions: SceneOptionsState): void => { + const applyTransientLightOptions = useCallback((nextOptions: SceneOptionsState, preview?: { shadow?: boolean }): void => { const scene = sceneRef.current; if (!scene) return; const nextDirectionalLight = directionalFromOptions(nextOptions); + const nextShadow = { maxExtend: nextOptions.shadowMaxExtend, lift: GALLERY_SHADOW_LIFT }; + const previewShadow = preview?.shadow !== false; if (nextOptions.textureLighting === "dynamic") { - scene.setOptions({ directionalLight: nextDirectionalLight }); + scene.setOptions({ + directionalLight: nextDirectionalLight, + ...(previewShadow ? { shadow: nextShadow } : {}), + }); } else { + if (previewShadow) scene.setOptions({ shadow: nextShadow }); (scene as BakedSolidLightingPreviewSceneHandle).previewBakedSolidLighting?.({ directionalLight: nextDirectionalLight, ambientLight: ambientFromOptions(nextOptions), + skipShadows: !previewShadow, }); } lightHandleRef.current?.setTransform({