General shifts

This commit is contained in:
2026-09-12 08:33:44 -07:00
parent 56f010a71e
commit 64c03a28a4
17 changed files with 176 additions and 27 deletions

View File

@@ -1,8 +1,10 @@
import * as THREE from 'three'
import { cardSceneDimensions } from './cardGeometry'
export type FinishName = 'Printed ink' | 'Foil' | 'Holographic'
export type SubstrateName = 'Paper' | 'Linen' | 'Plastic' | 'Metal' | 'Wood'
export const finishNames = ['Printed ink', 'Foil', 'Holographic', 'Gold leaf', 'Frosted glass'] as const
export const substrateNames = ['Paper', 'Linen', 'Metal', 'Wood', 'Leather'] as const
export type FinishName = typeof finishNames[number]
export type SubstrateName = typeof substrateNames[number] | 'Plastic'
export interface MaterialControls {
finish: FinishName
@@ -31,6 +33,7 @@ export function applyEdgeMaterialControls(
Plastic: { color: '#d4cab0', wornColor: '#c7ccd2', roughness: 0.24, metalness: 0, clearcoat: 0.72 },
Metal: { color: '#a8adb7', wornColor: '#d0d5dc', roughness: 0.2, metalness: 0.86, clearcoat: 0.18 },
Wood: { color: '#6b3f21', wornColor: '#a87847', roughness: 0.56, metalness: 0, clearcoat: 0.08 },
Leather: { color: '#503023', wornColor: '#976c49', roughness: 0.72, metalness: 0, clearcoat: 0.04 },
}
const look = edgeLooks[controls.substrate]
material.color.set(look.color)
@@ -134,6 +137,13 @@ const fragmentShader = `
return sin(phase) * visibility;
}
float leatherGrain(vec2 uv) {
vec2 p = uv * vec2(42.0, 58.8);
float visibility = 1.0 - smoothstep(0.2, 0.65, max(fwidth(p.x), fwidth(p.y)));
float pebbles = smoothstep(0.15, 0.85, fiberNoise(p));
return mix(0.5, pebbles, visibility);
}
// Height and analytic UV slopes keep thread ridges lit even at oblique angles.
vec3 linenRelief(vec2 uv, vec4 visibility) {
vec2 frequency = vec2(52.0, 73.0) * 6.2831853;
@@ -205,6 +215,7 @@ const fragmentShader = `
if (substrateMode == 2) wearColor = vec3(0.78, 0.80, 0.82);
if (substrateMode == 3) wearColor = vec3(0.72, 0.75, 0.80);
if (substrateMode == 4) wearColor = vec3(0.62, 0.39, 0.20);
if (substrateMode == 6) wearColor = vec3(0.59, 0.40, 0.25);
float abrasion = clamp(
edgeWear * 0.55 + scratchWear * 0.30 + scuffWear * 0.22,
0.0,
@@ -280,6 +291,8 @@ const fragmentShader = `
float woodSurfaceGrain = 0.5;
float metalEtchDepth = 0.0;
float metalEtchExposure = 1.0;
float leatherPebble = 0.5;
float leatherCoverage = smoothstep(0.08, 0.65, mask);
if (substrateMode == 0) {
vec2 fiberUv = artworkUv * vec2(230.0, 322.0);
float visible = 1.0 - smoothstep(0.4, 1.2, max(fwidth(fiberUv.x), fwidth(fiberUv.y)));
@@ -328,8 +341,33 @@ const fragmentShader = `
surfaceHeight = woodSurfaceGrain * relief;
surfaceShade = 0.88 + woodSurfaceGrain * 0.12;
}
if (substrateMode == 6) {
vec3 view = normalize(cameraPosition - vWorldPosition);
vec2 alongSurface = vec2(dot(view, normalize(vWorldTangent)),
dot(view, normalize(vWorldBitangent)));
float relief = 0.0032 * min(detail, 1.5);
float grain = leatherGrain(artworkUv);
vec2 parallax = alongSurface / max(abs(dot(view, normalize(vWorldNormal))), 0.35)
* (grain - 0.5) * relief / cardFaceSize;
leatherPebble = leatherGrain(artworkUv + parallax);
// Treat protected ink as a smooth printed layer, not recessed leather grain.
surfaceHeight = (leatherPebble - 0.5) * relief * leatherCoverage;
surfaceShade = 1.0 - (1.0 - leatherPebble) * 0.10 * min(detail, 1.5) * leatherCoverage;
}
// Only coating microrelief follows the finish mask; protected text stays flat.
if (finishMode == 3) {
float leafFold = filteredWave(artworkUv.x * 270.0
+ fiberNoise(artworkUv * vec2(28.0, 39.2)) * 16.0);
surfaceHeight += leafFold * 0.0012 * mask * finishStrength;
}
if (finishMode == 5) {
vec2 frostUv = artworkUv * vec2(190.0, 266.0);
float visible = 1.0 - smoothstep(0.4, 1.2, max(fwidth(frostUv.x), fwidth(frostUv.y)));
surfaceHeight += (fiberNoise(frostUv) - 0.5) * visible * 0.0025 * mask * finishStrength;
}
surfaceHeight -= scratchWear * 0.0018 + scuffWear * 0.0007;
float substrateNormalScale = substrateMode == 3 ? 0.06 : 0.12;
if (substrateMode == 6) substrateNormalScale = 0.0;
vec3 normal = normalize(
vWorldNormal +
vWorldTangent * normalSample.x * normalStrength * substrateNormalScale +
@@ -369,6 +407,7 @@ const fragmentShader = `
if (substrateMode == 2) substrateRoughness = min(roughness, 0.25);
if (substrateMode == 3) substrateRoughness = min(roughness, 0.20);
if (substrateMode == 4) substrateRoughness = max(roughness, 0.48);
if (substrateMode == 6) substrateRoughness = max(roughness, 0.62);
float nDotV = max(dot(normal, viewDirection), 0.0);
float dielectricFresnel = 0.04 + 0.96 * pow(1.0 - nDotV, 5.0);
@@ -420,6 +459,10 @@ const fragmentShader = `
vec3 woodTint = mix(vec3(0.72, 0.40, 0.18), vec3(1.0, 0.86, 0.65), woodSurfaceGrain);
printedInk *= mix(vec3(1.0), woodTint, 0.25);
}
if (substrateMode == 6) {
printedInk *= mix(vec3(1.0), vec3(0.98, 0.94, 0.88), leatherCoverage);
printedInk += lightColor * substrateSpecular * leatherPebble * 0.12 * leatherCoverage;
}
if (finishMode == 0) {
vec3 wornInk = applyWearColor(printedInk, edgeWear, scratchWear, scuffWear);
gl_FragColor = vec4(wornInk, artworkSample.a);
@@ -434,6 +477,32 @@ const fragmentShader = `
float specularPower = mix(96.0, 10.0, substrateRoughness);
float directSpecular = pow(max(dot(normal, halfVector), 0.0), specularPower);
float environmentBrightness = dot(environment, vec3(0.2126, 0.7152, 0.0722));
if (finishMode >= 3) {
vec3 treatment = printedInk;
if (finishMode == 3) {
float leafMottle = fiberNoise(artworkUv * vec2(38.0, 53.2));
vec3 gold = mix(vec3(0.76, 0.51, 0.19), vec3(0.92, 0.71, 0.34), leafMottle);
float inkLuma = dot(artworkSample.rgb, vec3(0.2126, 0.7152, 0.0722));
vec3 gilding = gold * (0.42 + direct * 0.30 + inkLuma * 0.28);
gilding += gold * environment * environmentIntensity * (0.30 + fresnel * 0.35);
gilding += lightColor * vec3(1.0, 0.85, 0.46)
* min(directSpecular * lightIntensity * attenuation * 0.40, 0.28);
treatment = mix(printedInk, gilding, 0.48);
}
if (finishMode == 5) {
float satin = pow(max(dot(normal, halfVector), 0.0), 8.0)
* diffuse * lightIntensity * attenuation;
vec3 coolFrost = vec3(0.79, 0.90, 0.95);
treatment = mix(printedInk, coolFrost * (0.38 + direct * 0.32), 0.32);
treatment += coolFrost * (satin * 0.22 + fresnel * environmentIntensity * 0.32);
}
vec3 coated = mix(printedInk, treatment, clamp(mask * finishStrength, 0.0, 1.0));
coated = applyWearColor(coated, edgeWear, scratchWear, scuffWear);
gl_FragColor = vec4(coated, artworkSample.a);
#include <tonemapping_fragment>
#include <colorspace_fragment>
return;
}
if (substrateMode == 3 && finishMode == 1) {
float plateSpecular = pow(max(dot(normal, halfVector), 0.0), 150.0);
float plateEdge = smoothstep(0.003, 0.055, fwidth(mask));
@@ -555,6 +624,8 @@ export function applyMaterialControls(
'Printed ink': 0,
Foil: 1,
Holographic: 2,
'Gold leaf': 3,
'Frosted glass': 5,
}
const substrateModes: Record<SubstrateName, number> = {
Paper: 0,
@@ -562,6 +633,7 @@ export function applyMaterialControls(
Plastic: 2,
Metal: 3,
Wood: 4,
Leather: 6,
}
material.uniforms.finishMode.value = finishModes[controls.finish]
material.uniforms.substrateMode.value = substrateModes[controls.substrate]

View File

@@ -7,6 +7,8 @@ import {
applyMaterialControls,
createCardMaterial,
createStudioCubeTexture,
finishNames,
substrateNames,
type FinishName,
type MaterialControls,
type SubstrateName,
@@ -101,17 +103,12 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
</label>
<label>Finish
<select id="finish">
<option>Holographic</option>
<option>Foil</option>
<option>Printed ink</option>
${finishNames.map(name => `<option${name === 'Holographic' ? ' selected' : ''}>${name}</option>`).join('')}
</select>
</label>
<label>Material
<select id="substrate">
<option>Paper</option>
<option>Linen</option>
<option>Metal</option>
<option>Wood</option>
${substrateNames.map(name => `<option>${name}</option>`).join('')}
</select>
</label>
<label>Pose
@@ -426,13 +423,13 @@ let applyingLightingPreset = false
const gui = new GUI({ title: 'Material proof' })
const surfaceFolder = gui.addFolder('Surface')
surfaceFolder.add(controls, 'finish', ['Printed ink', 'Foil', 'Holographic']).name('Finish').onChange((value: FinishName) => {
surfaceFolder.add(controls, 'finish', [...finishNames]).name('Finish').onChange((value: FinishName) => {
pauseScriptedMotion()
finishSelect.value = value
updateMaterial()
updateStatus()
})
surfaceFolder.add(controls, 'substrate', ['Paper', 'Linen', 'Metal', 'Wood']).name('Material').onChange((value: SubstrateName) => {
surfaceFolder.add(controls, 'substrate', [...substrateNames]).name('Material').onChange((value: SubstrateName) => {
pauseScriptedMotion()
substrateSelect.value = value
updateMaterial()
@@ -832,16 +829,9 @@ function isExperimentSnapshot(value: unknown): value is ExperimentSnapshot {
if (typeof value !== 'object' || value === null) return false
const snapshot = value as Record<string, unknown>
const fixtureValid = typeof snapshot.fixture === 'string' && snapshot.fixture.length > 0
const finishValid =
snapshot.finish === 'Printed ink' ||
snapshot.finish === 'Foil' ||
snapshot.finish === 'Holographic'
const substrateValid =
snapshot.substrate === 'Paper' ||
snapshot.substrate === 'Linen' ||
snapshot.substrate === 'Plastic' ||
snapshot.substrate === 'Metal' ||
snapshot.substrate === 'Wood'
const finishValid = finishNames.some(name => name === snapshot.finish)
const substrateValid = snapshot.substrate === 'Plastic' ||
substrateNames.some(name => name === snapshot.substrate)
const lightingPresetValid =
snapshot.lightingPreset === 'Studio' ||
snapshot.lightingPreset === 'Warm gallery' ||

View File

@@ -6,6 +6,8 @@ import {
applyEdgeMaterialControls,
applyMaterialControls,
createCardMaterial,
finishNames,
substrateNames,
type FinishName,
type SubstrateName,
} from './cardMaterial'
@@ -20,8 +22,8 @@ export interface PackCardSpec {
}
export const packSize = 10
export const packFinishes: readonly FinishName[] = ['Printed ink', 'Foil', 'Holographic']
export const packSubstrates: readonly SubstrateName[] = ['Paper', 'Linen', 'Metal', 'Wood']
export const packFinishes = finishNames
export const packSubstrates = substrateNames
function randomChoice<T>(values: readonly T[], random: () => number) {
const roll = random()