fix(web): decode textures as sRGB in 3D viewers
TextureLoader leaves colorSpace as NoColorSpace, so card, tile, token, model, and part art is uploaded as linear and then double-decoded in the shader, washing out contrast. Mark each texture sRGB so the GPU decodes it once, correctly.
This commit is contained in:
@@ -78,6 +78,13 @@ export function CardMesh({
|
|||||||
const face = useTexture(faceUrl ? assetUrl(faceUrl) : FALLBACK_URL);
|
const face = useTexture(faceUrl ? assetUrl(faceUrl) : FALLBACK_URL);
|
||||||
const back = useTexture(backUrl ? assetUrl(backUrl) : FALLBACK_URL);
|
const back = useTexture(backUrl ? assetUrl(backUrl) : FALLBACK_URL);
|
||||||
|
|
||||||
|
// Card art is sRGB-encoded. `TextureLoader` leaves `colorSpace` as
|
||||||
|
// `NoColorSpace`, which uploads the texture as linear and then double-decodes
|
||||||
|
// it in the shader, washing out contrast. Mark it sRGB so the GPU decodes it
|
||||||
|
// once, correctly. Clones (e.g. `flipTexture`) inherit this from the source.
|
||||||
|
face.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
back.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
// The face/back textures are shared (drei caches them by URL); each card's
|
// The face/back textures are shared (drei caches them by URL); each card's
|
||||||
// sprite cell is selected via a per-material UV transform injected into the
|
// sprite cell is selected via a per-material UV transform injected into the
|
||||||
// shader, so no per-card texture clone (and no re-upload) is needed. The
|
// shader, so no per-card texture clone (and no re-upload) is needed. The
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ function Tint({ root, tint }: { root: Object3D; tint: THREE.Color }) {
|
|||||||
function DiffuseTexture({ root, url }: { root: Object3D; url: string }) {
|
function DiffuseTexture({ root, url }: { root: Object3D; url: string }) {
|
||||||
const texture = useTexture(assetUrl(url));
|
const texture = useTexture(assetUrl(url));
|
||||||
|
|
||||||
|
// Diffuse art is sRGB-encoded; `TextureLoader` leaves `colorSpace` as
|
||||||
|
// `NoColorSpace`, which double-decodes it in the shader and washes out
|
||||||
|
// contrast. Mark it sRGB so the GPU decodes it once, correctly.
|
||||||
|
texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
// Apply the diffuse texture to every mesh material on the loaded model.
|
// Apply the diffuse texture to every mesh material on the loaded model.
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
root.traverse((child) => {
|
root.traverse((child) => {
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ export function TileMesh({
|
|||||||
}) {
|
}) {
|
||||||
const texture: THREE.Texture | null = url ? useTexture(assetUrl(url)) : null;
|
const texture: THREE.Texture | null = url ? useTexture(assetUrl(url)) : null;
|
||||||
|
|
||||||
|
// Tile art is sRGB-encoded; `TextureLoader` leaves `colorSpace` as
|
||||||
|
// `NoColorSpace`, which double-decodes it in the shader and washes out
|
||||||
|
// contrast. Mark it sRGB so the GPU decodes it once, correctly.
|
||||||
|
if (texture) texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
// Build the extruded geometry from the tile shape. When `stretch` is false
|
// Build the extruded geometry from the tile shape. When `stretch` is false
|
||||||
// and a texture is available, scale the shape to the image's aspect ratio so
|
// and a texture is available, scale the shape to the image's aspect ratio so
|
||||||
// the tile matches the source proportions instead of being square. Shared
|
// the tile matches the source proportions instead of being square. Shared
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export function TokenMesh({
|
|||||||
}) {
|
}) {
|
||||||
const texture: THREE.Texture | null = url ? useTexture(assetUrl(url)) : null;
|
const texture: THREE.Texture | null = url ? useTexture(assetUrl(url)) : null;
|
||||||
|
|
||||||
|
// Token art is sRGB-encoded; `TextureLoader` leaves `colorSpace` as
|
||||||
|
// `NoColorSpace`, which double-decodes it in the shader and washes out
|
||||||
|
// contrast. Mark it sRGB so the GPU decodes it once, correctly.
|
||||||
|
if (texture) texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
// Trace the image's alpha channel into a shape. Suspends until the trace
|
// Trace the image's alpha channel into a shape. Suspends until the trace
|
||||||
// resolves so the surrounding Suspense boundary (and `Bounds`) only mounts
|
// resolves so the surrounding Suspense boundary (and `Bounds`) only mounts
|
||||||
// once the token geometry is present. Falls back to a circle when there's no
|
// once the token geometry is present. Falls back to a circle when there's no
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ export function PartMesh({ part, baseUrl }: { part: Part; baseUrl?: string }) {
|
|||||||
const face = useTexture(faceUrl ? assetUrl(faceUrl) : FALLBACK_URL);
|
const face = useTexture(faceUrl ? assetUrl(faceUrl) : FALLBACK_URL);
|
||||||
const back = useTexture(backUrl ? assetUrl(backUrl) : FALLBACK_URL);
|
const back = useTexture(backUrl ? assetUrl(backUrl) : FALLBACK_URL);
|
||||||
|
|
||||||
|
// Part art is sRGB-encoded; `TextureLoader` leaves `colorSpace` as
|
||||||
|
// `NoColorSpace`, which double-decodes it in the shader and washes out
|
||||||
|
// contrast. Mark it sRGB so the GPU decodes it once, correctly. The sprite
|
||||||
|
// clones below inherit this from the source.
|
||||||
|
face.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
back.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
const trace = useTrace(shapeUrl, traceImage);
|
const trace = useTrace(shapeUrl, traceImage);
|
||||||
|
|
||||||
const { frontGeo, backGeo, wallsGeo } = useMemo(() => {
|
const { frontGeo, backGeo, wallsGeo } = useMemo(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user