fix: render gallery skybox unlit double-sided

This commit is contained in:
Tom Boullay
2026-05-25 17:53:46 +02:00
parent 6a412c7b00
commit d7351e5f37
3 changed files with 64 additions and 15 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ La galerie est disponible sur `/gallery`. Elle permet de parcourir les modèles
## Objectif ## Objectif
Cette page sert à remercier et valoriser le travail des designers du projet La Fabrik. Chaque modèle est affiché dans un canvas dédié, avec la même skybox que l'expérience principale pour garder une ambiance visuelle cohérente. Cette page sert à remercier et valoriser le travail des designers du projet La Fabrik. Chaque modèle est affiché dans un canvas dédié, avec la même skybox et le même lighting que l'expérience principale.
## Utilisation ## Utilisation
@@ -19,7 +19,7 @@ Cette page sert à remercier et valoriser le travail des designers du projet La
- Le viewer utilise `@react-three/fiber` et `@react-three/drei`. - Le viewer utilise `@react-three/fiber` et `@react-three/drei`.
- `OrbitControls` permet de manipuler la caméra autour du modèle. - `OrbitControls` permet de manipuler la caméra autour du modèle.
- `Bounds` et `Center` recadrent automatiquement le modèle actif. - `Bounds` et `Center` recadrent automatiquement le modèle actif.
- `SkyModel` réutilise la skybox du jeu. - `SkyModel` réutilise la skybox du jeu, avec un matériau non éclairé uniquement dans la galerie pour éviter que certaines faces deviennent noires avec une caméra orbitale libre.
- Les animations GLTF présentes dans un modèle sont lancées automatiquement. - Les animations GLTF présentes dans un modèle sont lancées automatiquement.
- Un diagnostic simple inspecte les matériaux chargés pour signaler les textures absentes ou non exploitables. - Un diagnostic simple inspecte les matériaux chargés pour signaler les textures absentes ou non exploitables.
+57 -10
View File
@@ -8,12 +8,16 @@ interface SkyModelProps {
modelPath: string; modelPath: string;
fallbackModelPath?: string | undefined; fallbackModelPath?: string | undefined;
fallbackScale?: number | undefined; fallbackScale?: number | undefined;
materialSide?: THREE.Side | undefined;
scale?: number | undefined; scale?: number | undefined;
unlit?: boolean | undefined;
} }
interface SkyModelContentProps { interface SkyModelContentProps {
materialSide: THREE.Side;
modelPath: string; modelPath: string;
scale: number; scale: number;
unlit: boolean;
} }
interface SkyModelErrorBoundaryProps { interface SkyModelErrorBoundaryProps {
@@ -54,23 +58,37 @@ class SkyModelErrorBoundary extends Component<
export function SkyModel({ export function SkyModel({
fallbackModelPath, fallbackModelPath,
fallbackScale = SKY_MODEL_SCALE, fallbackScale = SKY_MODEL_SCALE,
materialSide = THREE.BackSide,
modelPath, modelPath,
scale = SKY_MODEL_SCALE, scale = SKY_MODEL_SCALE,
unlit = false,
}: SkyModelProps): React.JSX.Element { }: SkyModelProps): React.JSX.Element {
const fallback = fallbackModelPath ? ( const fallback = fallbackModelPath ? (
<SkyModelContent modelPath={fallbackModelPath} scale={fallbackScale} /> <SkyModelContent
materialSide={materialSide}
modelPath={fallbackModelPath}
scale={fallbackScale}
unlit={unlit}
/>
) : null; ) : null;
return ( return (
<SkyModelErrorBoundary key={modelPath} fallback={fallback}> <SkyModelErrorBoundary key={modelPath} fallback={fallback}>
<SkyModelContent modelPath={modelPath} scale={scale} /> <SkyModelContent
materialSide={materialSide}
modelPath={modelPath}
scale={scale}
unlit={unlit}
/>
</SkyModelErrorBoundary> </SkyModelErrorBoundary>
); );
} }
function SkyModelContent({ function SkyModelContent({
materialSide,
modelPath, modelPath,
scale, scale,
unlit,
}: SkyModelContentProps): React.JSX.Element { }: SkyModelContentProps): React.JSX.Element {
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
@@ -78,7 +96,10 @@ function SkyModelContent({
scope: "SkyModel", scope: "SkyModel",
scale, scale,
}); });
const model = useMemo(() => createSkyModel(scene), [scene]); const model = useMemo(
() => createSkyModel(scene, materialSide, unlit),
[materialSide, scene, unlit],
);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -102,7 +123,11 @@ function SkyModelContent({
); );
} }
function createSkyModel(scene: THREE.Object3D): THREE.Object3D { function createSkyModel(
scene: THREE.Object3D,
materialSide: THREE.Side,
unlit: boolean,
): THREE.Object3D {
const model = scene.clone(true); const model = scene.clone(true);
model.traverse((object) => { model.traverse((object) => {
@@ -112,20 +137,42 @@ function createSkyModel(scene: THREE.Object3D): THREE.Object3D {
if (!(object instanceof THREE.Mesh)) return; if (!(object instanceof THREE.Mesh)) return;
object.material = Array.isArray(object.material) object.material = Array.isArray(object.material)
? object.material.map(createSkyMaterial) ? object.material.map((material) =>
: createSkyMaterial(object.material); createSkyMaterial(material, materialSide, unlit),
)
: createSkyMaterial(object.material, materialSide, unlit);
}); });
return model; return model;
} }
function createSkyMaterial<T extends THREE.Material>(material: T): T { function createSkyMaterial<T extends THREE.Material>(
const skyMaterial = material.clone(); material: T,
skyMaterial.side = THREE.BackSide; materialSide: THREE.Side,
unlit: boolean,
): THREE.Material {
const skyMaterial = unlit
? createUnlitSkyMaterial(material)
: material.clone();
skyMaterial.side = materialSide;
skyMaterial.depthTest = false; skyMaterial.depthTest = false;
skyMaterial.depthWrite = false; skyMaterial.depthWrite = false;
return skyMaterial as T; return skyMaterial;
}
function createUnlitSkyMaterial(
material: THREE.Material,
): THREE.MeshBasicMaterial {
const sourceMaterial = material as THREE.MeshStandardMaterial;
return new THREE.MeshBasicMaterial({
color: sourceMaterial.color?.clone() ?? new THREE.Color("#ffffff"),
map: sourceMaterial.map ?? null,
opacity: sourceMaterial.opacity,
toneMapped: false,
transparent: sourceMaterial.transparent,
});
} }
function disposeSkyModelMaterials(model: THREE.Object3D): void { function disposeSkyModelMaterials(model: THREE.Object3D): void {
+5 -3
View File
@@ -23,13 +23,14 @@ import {
} from "lucide-react"; } from "lucide-react";
import * as THREE from "three"; import * as THREE from "three";
import { SkyModel } from "@/components/three/world/SkyModel"; import { SkyModel } from "@/components/three/world/SkyModel";
import { galleryModels, type GalleryModel } from "@/data/galleryModels";
import { import {
GAME_SCENE_FALLBACK_SKY_MODEL_PATH, GAME_SCENE_FALLBACK_SKY_MODEL_PATH,
GAME_SCENE_FALLBACK_SKY_MODEL_SCALE, GAME_SCENE_FALLBACK_SKY_MODEL_SCALE,
GAME_SCENE_SKY_MODEL_PATH, GAME_SCENE_SKY_MODEL_PATH,
GAME_SCENE_SKY_MODEL_SCALE, GAME_SCENE_SKY_MODEL_SCALE,
} from "@/data/world/environmentConfig"; } from "@/data/world/environmentConfig";
import { galleryModels, type GalleryModel } from "@/data/galleryModels"; import { Lighting } from "@/world/Lighting";
interface GalleryModelProps { interface GalleryModelProps {
model: GalleryModel; model: GalleryModel;
@@ -147,11 +148,12 @@ function GalleryScene({
<SkyModel <SkyModel
fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH} fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH}
fallbackScale={GAME_SCENE_FALLBACK_SKY_MODEL_SCALE} fallbackScale={GAME_SCENE_FALLBACK_SKY_MODEL_SCALE}
materialSide={THREE.DoubleSide}
modelPath={GAME_SCENE_SKY_MODEL_PATH} modelPath={GAME_SCENE_SKY_MODEL_PATH}
scale={GAME_SCENE_SKY_MODEL_SCALE} scale={GAME_SCENE_SKY_MODEL_SCALE}
unlit
/> />
<ambientLight intensity={0.75} /> <Lighting />
<directionalLight position={[6, 8, 4]} intensity={2.1} />
<Bounds fit clip observe margin={1.35}> <Bounds fit clip observe margin={1.35}>
<Center> <Center>
<GalleryModelPreview <GalleryModelPreview