Rework Byzantine hall as a late-antique basilica interior.

Replace the flat gold-mosaic chapel with marble revetment walls, a
Cosmatesque stone floor, and a coffered timber ceiling, matching
Santa Sabina / San Vitale reference interiors.

New procedural surfaces in galleryProceduralTextures.ts:
- marble-revetment: book-matched veined panels in moulded travertine
  surrounds (each panel mirrored about its centre)
- marble-opus-sectile: cut-stone bays with diagonal diamond inlays and
  corner triangles in porphyry, verd-antique, giallo, and grey
- coffered-wood: recessed walnut coffers with lit beam chamfers

Add a byzantine detail set to MovementHallDetails.tsx: engaged porphyry
colonnettes with white-marble basket capitals and impost blocks carrying
the ceiling, a marble revetment dado, and hanging brass polycandela.
Colonnettes stay flush in the corner pockets so they do not clip the
~0.7m frame hang margin.

Basilicas are lit by shafts from high windows rather than evenly
flooded, but the shared hall lights carry a brightness floor that
prevented dimming. Add an opt-in lightScale to the interior style that
scales the scene, hall, track, and HDR environment lights. It defaults
to 1, so all other halls are unchanged; only Byzantine opts down. Window
fill lights are left unscaled so the daylight shafts still read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Danila KhodjaefandClaude Opus 5 committed 2026-07-31 18:59:56 +03:00
1 parent 1853222e01
commit 1cb9eef935
5 files changed
+400 -37

No files matched your search

@@ -139,6 +139,106 @@ function MedievalDetails({ halfW, halfD }: Pick<Props, 'halfW' | 'halfD'>) {
);
}
function ByzantineDetails({ halfW, halfD, trim }: Pick<Props, 'halfW' | 'halfD'> & { trim: string }) {
// Engaged porphyry colonnettes with Byzantine basket/impost capitals at the
// corners (flush — never proud of the ~0.7m frame hang margin), a marble
// revetment dado at spring-line height, and hanging brass polycandela in
// place of wall torches (a Byzantine basilica hangs its light, not sconces it).
const colonnettes = useMemo(
() =>
[
[-halfW + 0.08, -halfD + 0.08],
[halfW - 0.08, -halfD + 0.08],
[-halfW + 0.08, halfD - 0.08],
[halfW - 0.08, halfD - 0.08],
] as [number, number][],
[halfW, halfD]
);
const lampPositions = useMemo(
() =>
[
[0, -halfD * 0.32],
[0, halfD * 0.18],
] as [number, number][],
[halfD]
);
const dadoW = Math.max(0, halfW * 2 - 0.3);
const dadoD = Math.max(0, halfD * 2 - 0.3);
return (
<group>
{/* Marble revetment dado — imperial banding course at chair-rail height */}
{[
[0, -halfD + 0.07, dadoW, 0.1] as const,
[0, halfD - 0.07, dadoW, 0.1] as const,
[-halfW + 0.07, 0, 0.1, dadoD] as const,
[halfW - 0.07, 0, 0.1, dadoD] as const,
].map(([x, z, w, d], i) => (
<group key={i}>
<mesh position={[x, 0.62, z]}>
<boxGeometry args={[w, 1.24, d]} />
<meshStandardMaterial color="#6d5644" roughness={0.34} metalness={0.12} />
</mesh>
<mesh position={[x, 1.28, z]}>
<boxGeometry args={[w * 1.004, 0.09, d * 1.004]} />
<meshStandardMaterial color="#d8c8a8" roughness={0.42} metalness={0.1} />
</mesh>
</group>
))}
{/* Engaged colonnettes, basket capital, and impost block */}
{colonnettes.map(([x, z], i) => (
<group key={i} position={[x, 0, z]}>
{/* Polished porphyry shaft — the imperial stone of the reference basilicas */}
<mesh position={[0, 1.95, 0]}>
<cylinderGeometry args={[0.14, 0.16, 3.4, 14]} />
<meshStandardMaterial color="#43222a" roughness={0.32} metalness={0.16} />
</mesh>
<mesh position={[0, 0.12, 0]}>
<cylinderGeometry args={[0.17, 0.19, 0.24, 14]} />
<meshStandardMaterial color="#d8cdb4" roughness={0.5} metalness={0.08} />
</mesh>
{/* Basket capital in white marble, then an impost block carrying the ceiling */}
<mesh position={[0, 3.8, 0]}>
<cylinderGeometry args={[0.22, 0.11, 0.3, 8]} />
<meshStandardMaterial color="#e8e0cc" roughness={0.44} metalness={0.06} />
</mesh>
<mesh position={[0, 4.05, 0]}>
<boxGeometry args={[0.28, 0.22, 0.28]} />
<meshStandardMaterial color="#ded4bc" roughness={0.5} metalness={0.05} />
</mesh>
</group>
))}
{/* Hanging polycandela — brass ring lamps suspended from the vault */}
{lampPositions.map(([x, z], i) => (
<group key={i} position={[x, 0, z]}>
<mesh position={[0, 4.1, 0]}>
<cylinderGeometry args={[0.012, 0.012, 0.5, 6]} />
<meshStandardMaterial color="#5a4a20" roughness={0.4} metalness={0.6} />
</mesh>
<mesh position={[0, 3.84, 0]} rotation={[Math.PI / 2, 0, 0]}>
<torusGeometry args={[0.32, 0.025, 8, 20]} />
<meshStandardMaterial color={trim} roughness={0.25} metalness={0.8} emissive="#3a2808" emissiveIntensity={0.1} />
</mesh>
{Array.from({ length: 6 }, (_, j) => {
const a = (j / 6) * Math.PI * 2;
return (
<mesh key={j} position={[Math.cos(a) * 0.32, 3.76, Math.sin(a) * 0.32]}>
<sphereGeometry args={[0.045, 8, 8]} />
<meshStandardMaterial color="#ffcf80" emissive="#ff9830" emissiveIntensity={0.9} toneMapped={false} />
</mesh>
);
})}
<pointLight position={[0, 3.76, 0]} intensity={0.85} distance={6} color="#ffb060" />
</group>
))}
</group>
);
}
function NeoclassicalDetails({ halfW, halfD, trim }: Pick<Props, 'halfW' | 'halfD'> & { trim: string }) {
// Shallow engaged corner pilasters only. Freestanding columns (~0.5m into the
// room) overlap the outermost frames — hang margin is only ~0.7m from each end.
@@ -226,6 +326,8 @@ export default function MovementHallDetails({ style, width, depth, halfW, halfD
return <BaroqueDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
case 'medieval':
return <MedievalDetails halfW={halfW} halfD={halfD} />;
case 'byzantine':
return <ByzantineDetails halfW={halfW} halfD={halfD} trim={trim} />;
case 'neoclassical':
return <NeoclassicalDetails halfW={halfW} halfD={halfD} trim={trim} />;
case 'classical':
+12 -10
View File
@@ -1484,6 +1484,7 @@ function ArtistHall({
);
const titleColor = interiorStyle?.titleColor ?? '#4a3020';
const warmLight = interiorStyle?.warmLight ?? '#fff5e8';
const hallLightScale = interiorStyle?.lightScale ?? 1;
const wallRoughness = interiorStyle ? 0.75 : 0.92;
const wallMetalness = interiorStyle ? 0.08 : 0.06;
@@ -1656,7 +1657,7 @@ function ArtistHall({
<GalleryTrackLights
width={width}
depth={depth}
intensity={Math.max(1.8, interiorStyle.trackLights * 2.8) * Math.PI}
intensity={Math.max(1.8, interiorStyle.trackLights * 2.8) * Math.PI * interiorStyle.lightScale}
color={interiorStyle.warmLight}
/>
</>
@@ -1736,14 +1737,14 @@ function ArtistHall({
<pointLight
position={[0, WALL_HEIGHT - 0.4, 0]}
intensity={(interiorStyle ? Math.max(1.0, interiorStyle.ambient * 1.8) : 0.7) * Math.PI}
intensity={(interiorStyle ? Math.max(1.0, interiorStyle.ambient * 1.8) : 0.7) * Math.PI * hallLightScale}
distance={width + depth}
decay={0}
color={warmLight}
/>
<pointLight
position={[0, WALL_HEIGHT - 0.4, -halfD / 2]}
intensity={(interiorStyle ? Math.max(0.7, interiorStyle.ambient * 1.2) : 0.5) * Math.PI}
intensity={(interiorStyle ? Math.max(0.7, interiorStyle.ambient * 1.2) : 0.5) * Math.PI * hallLightScale}
distance={Math.max(12, depth * 0.75)}
decay={0}
color={warmLight}
@@ -1751,7 +1752,7 @@ function ArtistHall({
{interiorStyle && (
<directionalLight
position={[0, 6, -halfD - 2]}
intensity={Math.max(0.55, interiorStyle.ambient * 0.85) * Math.PI}
intensity={Math.max(0.55, interiorStyle.ambient * 0.85) * Math.PI * hallLightScale}
color={interiorStyle.sunLight}
/>
)}
@@ -2431,7 +2432,8 @@ export default function VirtualGallery(props: Props) {
const sceneFog = interiorStyle?.fog ?? '#0d0906';
// Floor ambient so dark period styles (Gothic, Byzantine) stay readable without HDR IBL.
// three r155+ physical lights need ~π× legacy intensity for similar brightness.
const ambientIntensity = Math.max(0.85, interiorStyle?.ambient ?? 0.55) * Math.PI;
const lightScale = interiorStyle?.lightScale ?? 1;
const ambientIntensity = Math.max(0.85, interiorStyle?.ambient ?? 0.55) * Math.PI * lightScale;
const handleCanvasPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
if (!active || showExitNav || e.button !== 0) return;
@@ -2546,22 +2548,22 @@ export default function VirtualGallery(props: Props) {
<hemisphereLight
color={interiorStyle?.warmLight ?? '#fff8f0'}
groundColor={sceneFog}
intensity={0.55 * Math.PI}
intensity={0.55 * Math.PI * lightScale}
/>
<directionalLight
position={interiorStyle ? [2, 10, -4] : [3, 9, 4]}
intensity={(interiorStyle ? 1.4 : 0.9) * Math.PI}
intensity={(interiorStyle ? 1.4 : 0.9) * Math.PI * lightScale}
color={interiorStyle?.sunLight ?? '#fff8f0'}
/>
<directionalLight
position={[-4, 6, 5]}
intensity={0.55 * Math.PI}
intensity={0.55 * Math.PI * lightScale}
color={interiorStyle?.warmLight ?? '#fff5e8'}
/>
{/* Guaranteed fill so walls never disappear if HDR Environment fails. */}
<pointLight
position={[0, 3.2, 0]}
intensity={2.2 * Math.PI}
intensity={2.2 * Math.PI * lightScale}
distance={40}
decay={0}
color={interiorStyle?.warmLight ?? '#fff5e8'}
@@ -2573,7 +2575,7 @@ export default function VirtualGallery(props: Props) {
<Suspense fallback={null}>
<Environment
preset={interiorStyle?.details === 'modern' || interiorStyle?.details === 'industrial' ? 'city' : 'warehouse'}
environmentIntensity={interiorStyle ? 0.55 : 0.25}
environmentIntensity={(interiorStyle ? 0.55 : 0.25) * lightScale}
/>
</Suspense>
</SceneErrorBoundary>