feat: add gallery lighting controls
This commit is contained in:
@@ -11,7 +11,8 @@ Cette page sert à remercier et valoriser le travail des designers du projet La
|
|||||||
1. Ouvrir `/gallery`.
|
1. Ouvrir `/gallery`.
|
||||||
2. Utiliser les flèches en bas de l'écran pour passer au modèle précédent ou suivant.
|
2. Utiliser les flèches en bas de l'écran pour passer au modèle précédent ou suivant.
|
||||||
3. Tourner autour du modèle avec la souris ou le doigt.
|
3. Tourner autour du modèle avec la souris ou le doigt.
|
||||||
4. Lire le diagnostic texture discret pour savoir si le modèle chargé semble correct côté textures.
|
4. Utiliser le bouton de réglages à droite pour ouvrir ou fermer le panneau lumière.
|
||||||
|
5. Lire le diagnostic texture discret pour savoir si le modèle chargé semble correct côté textures.
|
||||||
|
|
||||||
## Fonctionnement
|
## Fonctionnement
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ Cette page sert à remercier et valoriser le travail des designers du projet La
|
|||||||
- `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, avec un matériau non éclairé uniquement dans la galerie pour éviter que certaines faces deviennent noires avec une caméra orbitale libre.
|
- `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 lumières reprennent les valeurs par défaut du jeu, puis peuvent être ajustées dans le panneau latéral.
|
||||||
|
- `OrbitControls` autorise une orbite verticale complète pour inspecter le dessous des modèles.
|
||||||
- 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.
|
||||||
|
|
||||||
|
|||||||
+105
@@ -169,6 +169,107 @@ canvas {
|
|||||||
color: rgba(226, 232, 240, 0.72);
|
color: rgba(226, 232, 240, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gallery-light-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 108px;
|
||||||
|
right: 0;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
transform: translateX(260px);
|
||||||
|
transition: transform 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-panel.is-open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-panel-toggle {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border: 1px solid rgba(248, 250, 252, 0.14);
|
||||||
|
border-right: 0;
|
||||||
|
border-radius: 999px 0 0 999px;
|
||||||
|
background: rgba(3, 7, 18, 0.68);
|
||||||
|
color: rgba(248, 250, 252, 0.84);
|
||||||
|
cursor: pointer;
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-panel-toggle:hover,
|
||||||
|
.gallery-light-panel-toggle:focus-visible {
|
||||||
|
color: #f8fafc;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-panel-content {
|
||||||
|
width: 236px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid rgba(248, 250, 252, 0.14);
|
||||||
|
border-right: 0;
|
||||||
|
border-radius: 18px 0 0 18px;
|
||||||
|
background: rgba(3, 7, 18, 0.72);
|
||||||
|
box-shadow: 0 18px 52px rgba(0, 0, 0, 0.28);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-panel-content header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-panel-content header span {
|
||||||
|
color: rgba(248, 250, 252, 0.86);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-panel-content header button {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(203, 213, 225, 0.72);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-panel-content header button:hover,
|
||||||
|
.gallery-light-panel-content header button:focus-visible {
|
||||||
|
color: #f8fafc;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-control {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-control span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: rgba(226, 232, 240, 0.78);
|
||||||
|
font-family: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-control strong {
|
||||||
|
color: rgba(248, 250, 252, 0.88);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-light-control input {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: #dbeafe;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.gallery-title {
|
.gallery-title {
|
||||||
right: 50%;
|
right: 50%;
|
||||||
@@ -196,6 +297,10 @@ canvas {
|
|||||||
left: auto;
|
left: auto;
|
||||||
transform: translateX(50%);
|
transform: translateX(50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gallery-light-panel {
|
||||||
|
top: 78px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Docs layout */
|
/* Docs layout */
|
||||||
|
|||||||
+142
-5
@@ -19,27 +19,53 @@ import {
|
|||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
SlidersHorizontal,
|
||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
} 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 { galleryModels, type GalleryModel } from "@/data/galleryModels";
|
||||||
|
import {
|
||||||
|
AMBIENT_LIGHT_COLOR,
|
||||||
|
LIGHTING_DEFAULTS,
|
||||||
|
SUN_LIGHT_COLOR,
|
||||||
|
} from "@/data/world/lightingConfig";
|
||||||
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 { Lighting } from "@/world/Lighting";
|
|
||||||
|
|
||||||
interface GalleryModelProps {
|
interface GalleryModelProps {
|
||||||
model: GalleryModel;
|
model: GalleryModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GallerySceneProps extends GalleryModelProps {
|
interface GallerySceneProps extends GalleryModelProps {
|
||||||
|
lighting: GalleryLightingConfig;
|
||||||
onTextureDiagnosticReady: (diagnostic: TextureDiagnostic) => void;
|
onTextureDiagnosticReady: (diagnostic: TextureDiagnostic) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GalleryModelPreviewProps extends GalleryModelProps {
|
||||||
|
onTextureDiagnosticReady: (diagnostic: TextureDiagnostic) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GalleryLightingConfig {
|
||||||
|
ambientIntensity: number;
|
||||||
|
sunIntensity: number;
|
||||||
|
sunX: number;
|
||||||
|
sunY: number;
|
||||||
|
sunZ: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GalleryLightControl {
|
||||||
|
key: keyof GalleryLightingConfig;
|
||||||
|
label: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface TextureDiagnostic {
|
interface TextureDiagnostic {
|
||||||
modelId: string | null;
|
modelId: string | null;
|
||||||
status: "loading" | "ok" | "warning";
|
status: "loading" | "ok" | "warning";
|
||||||
@@ -71,6 +97,14 @@ const LOADING_TEXTURE_DIAGNOSTIC: TextureDiagnostic = {
|
|||||||
summary: "Analyse des textures...",
|
summary: "Analyse des textures...",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GALLERY_LIGHT_CONTROLS: GalleryLightControl[] = [
|
||||||
|
{ key: "ambientIntensity", label: "Ambiance", min: 0, max: 5, step: 0.1 },
|
||||||
|
{ key: "sunIntensity", label: "Soleil", min: 0, max: 8, step: 0.1 },
|
||||||
|
{ key: "sunX", label: "Soleil X", min: -100, max: 100, step: 1 },
|
||||||
|
{ key: "sunY", label: "Soleil Y", min: -100, max: 150, step: 1 },
|
||||||
|
{ key: "sunZ", label: "Soleil Z", min: -100, max: 100, step: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
class GalleryViewerErrorBoundary extends Component<
|
class GalleryViewerErrorBoundary extends Component<
|
||||||
GalleryViewerErrorBoundaryProps,
|
GalleryViewerErrorBoundaryProps,
|
||||||
GalleryViewerErrorBoundaryState
|
GalleryViewerErrorBoundaryState
|
||||||
@@ -106,7 +140,7 @@ class GalleryViewerErrorBoundary extends Component<
|
|||||||
function GalleryModelPreview({
|
function GalleryModelPreview({
|
||||||
model,
|
model,
|
||||||
onTextureDiagnosticReady,
|
onTextureDiagnosticReady,
|
||||||
}: GallerySceneProps): React.JSX.Element {
|
}: GalleryModelPreviewProps): React.JSX.Element {
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const { animations, scene } = useGLTF(model.path);
|
const { animations, scene } = useGLTF(model.path);
|
||||||
const modelScene = useMemo(() => scene.clone(true), [scene]);
|
const modelScene = useMemo(() => scene.clone(true), [scene]);
|
||||||
@@ -140,6 +174,7 @@ function GalleryModelPreview({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function GalleryScene({
|
function GalleryScene({
|
||||||
|
lighting,
|
||||||
model,
|
model,
|
||||||
onTextureDiagnosticReady,
|
onTextureDiagnosticReady,
|
||||||
}: GallerySceneProps): React.JSX.Element {
|
}: GallerySceneProps): React.JSX.Element {
|
||||||
@@ -153,7 +188,7 @@ function GalleryScene({
|
|||||||
scale={GAME_SCENE_SKY_MODEL_SCALE}
|
scale={GAME_SCENE_SKY_MODEL_SCALE}
|
||||||
unlit
|
unlit
|
||||||
/>
|
/>
|
||||||
<Lighting />
|
<GalleryLighting lighting={lighting} />
|
||||||
<Bounds fit clip observe margin={1.35}>
|
<Bounds fit clip observe margin={1.35}>
|
||||||
<Center>
|
<Center>
|
||||||
<GalleryModelPreview
|
<GalleryModelPreview
|
||||||
@@ -167,8 +202,28 @@ function GalleryScene({
|
|||||||
enableDamping
|
enableDamping
|
||||||
autoRotate
|
autoRotate
|
||||||
autoRotateSpeed={0.5}
|
autoRotateSpeed={0.5}
|
||||||
minPolarAngle={Math.PI * 0.18}
|
minPolarAngle={0}
|
||||||
maxPolarAngle={Math.PI * 0.48}
|
maxPolarAngle={Math.PI}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GalleryLighting({
|
||||||
|
lighting,
|
||||||
|
}: {
|
||||||
|
lighting: GalleryLightingConfig;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ambientLight
|
||||||
|
intensity={lighting.ambientIntensity}
|
||||||
|
color={AMBIENT_LIGHT_COLOR}
|
||||||
|
/>
|
||||||
|
<directionalLight
|
||||||
|
position={[lighting.sunX, lighting.sunY, lighting.sunZ]}
|
||||||
|
intensity={lighting.sunIntensity}
|
||||||
|
color={SUN_LIGHT_COLOR}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -192,6 +247,62 @@ function TextureStatusBadge({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GalleryLightingPanel({
|
||||||
|
lighting,
|
||||||
|
onChange,
|
||||||
|
onReset,
|
||||||
|
onToggle,
|
||||||
|
open,
|
||||||
|
}: {
|
||||||
|
lighting: GalleryLightingConfig;
|
||||||
|
onChange: (key: keyof GalleryLightingConfig, value: number) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
onToggle: () => void;
|
||||||
|
open: boolean;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<aside className={`gallery-light-panel ${open ? "is-open" : ""}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-light-panel-toggle"
|
||||||
|
onClick={onToggle}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-label={
|
||||||
|
open ? "Fermer les réglages lumière" : "Ouvrir les réglages lumière"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SlidersHorizontal aria-hidden="true" size={18} strokeWidth={1.8} />
|
||||||
|
</button>
|
||||||
|
<div className="gallery-light-panel-content" aria-hidden={!open}>
|
||||||
|
<header>
|
||||||
|
<span>LIGHTS</span>
|
||||||
|
<button type="button" onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
{GALLERY_LIGHT_CONTROLS.map((control) => (
|
||||||
|
<label key={control.key} className="gallery-light-control">
|
||||||
|
<span>
|
||||||
|
{control.label}
|
||||||
|
<strong>{lighting[control.key].toFixed(1)}</strong>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={control.min}
|
||||||
|
max={control.max}
|
||||||
|
step={control.step}
|
||||||
|
value={lighting[control.key]}
|
||||||
|
onChange={(event) =>
|
||||||
|
onChange(control.key, Number(event.currentTarget.value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getTextureDiagnostic(
|
function getTextureDiagnostic(
|
||||||
modelId: string,
|
modelId: string,
|
||||||
modelScene: THREE.Object3D,
|
modelScene: THREE.Object3D,
|
||||||
@@ -247,6 +358,10 @@ function getTextureDiagnostic(
|
|||||||
|
|
||||||
export function GalleryPage(): React.JSX.Element {
|
export function GalleryPage(): React.JSX.Element {
|
||||||
const [activeModelIndex, setActiveModelIndex] = useState(0);
|
const [activeModelIndex, setActiveModelIndex] = useState(0);
|
||||||
|
const [lightPanelOpen, setLightPanelOpen] = useState(false);
|
||||||
|
const [lighting, setLighting] = useState<GalleryLightingConfig>({
|
||||||
|
...LIGHTING_DEFAULTS,
|
||||||
|
});
|
||||||
const [textureDiagnostic, setTextureDiagnostic] = useState<TextureDiagnostic>(
|
const [textureDiagnostic, setTextureDiagnostic] = useState<TextureDiagnostic>(
|
||||||
LOADING_TEXTURE_DIAGNOSTIC,
|
LOADING_TEXTURE_DIAGNOSTIC,
|
||||||
);
|
);
|
||||||
@@ -269,6 +384,20 @@ export function GalleryPage(): React.JSX.Element {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLightChange = (
|
||||||
|
key: keyof GalleryLightingConfig,
|
||||||
|
value: number,
|
||||||
|
): void => {
|
||||||
|
setLighting((currentLighting) => ({
|
||||||
|
...currentLighting,
|
||||||
|
[key]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetLighting = (): void => {
|
||||||
|
setLighting({ ...LIGHTING_DEFAULTS });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="gallery-page">
|
<main className="gallery-page">
|
||||||
<h1 className="gallery-title">GALERIE</h1>
|
<h1 className="gallery-title">GALERIE</h1>
|
||||||
@@ -278,6 +407,7 @@ export function GalleryPage(): React.JSX.Element {
|
|||||||
<Canvas camera={{ position: [3.5, 2.4, 4.5], fov: 45 }} dpr={[1, 2]}>
|
<Canvas camera={{ position: [3.5, 2.4, 4.5], fov: 45 }} dpr={[1, 2]}>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<GalleryScene
|
<GalleryScene
|
||||||
|
lighting={lighting}
|
||||||
model={activeModel}
|
model={activeModel}
|
||||||
onTextureDiagnosticReady={setTextureDiagnostic}
|
onTextureDiagnosticReady={setTextureDiagnostic}
|
||||||
/>
|
/>
|
||||||
@@ -310,6 +440,13 @@ export function GalleryPage(): React.JSX.Element {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<TextureStatusBadge diagnostic={activeTextureDiagnostic} />
|
<TextureStatusBadge diagnostic={activeTextureDiagnostic} />
|
||||||
|
<GalleryLightingPanel
|
||||||
|
lighting={lighting}
|
||||||
|
onChange={handleLightChange}
|
||||||
|
onReset={resetLighting}
|
||||||
|
onToggle={() => setLightPanelOpen((open) => !open)}
|
||||||
|
open={lightPanelOpen}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user