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

@@ -112,8 +112,8 @@ Changing a light type, color, position, intensity, environment contribution, or
| Control | Default | Available range |
|---|---:|---:|
| Finish | Holographic | Printed ink / Foil / Holographic |
| Material | Paper | Paper / Linen / Metal / Wood |
| Finish | Holographic | Printed ink / Foil / Holographic / Gold leaf / Frosted glass |
| Material | Paper | Paper / Linen / Metal / Wood / Leather |
| Finish strength | 0.60 | 0-1 |
| Roughness | 0.23 | 0.05-0.8 |
| Surface detail | 0.14 | 0-0.45 |
@@ -123,6 +123,16 @@ Changing a light type, color, position, intensity, environment contribution, or
Finish and material are independent concepts, but combinations should exist only when they have an intentional visual interpretation.
Leather, Gold leaf, and Frosted glass are experimental
September 11 additions awaiting visual review. They do not change the existing
default selection or approved recipes. Leather applies generic substrate
relief and edge treatments with grain suppressed over mask-protected ink; the two
new coatings follow each card's finish mask. Vellum and Pearlescent were removed
after visual review; Leather's initial aggressive grain was replaced with shallow,
softly filtered grain pending further approval.
Frosted glass is an opaque etched coating, not a transparent card material. See the
[experimental treatment descriptions](README.md#experimental-materials-and-finishes).
## Substrate Recipes
### Paper

View File

@@ -27,6 +27,32 @@ Transform gizmos and a general timeline are intentionally deferred. The separate
## Tear-open foil pack prototype
### Experimental materials and finishes
The toolbar, Lab controls, saved experiments, and random pack pool share the same
available choices. The following treatments are visual trials, not approved defaults:
- **Leather material:** shallow, softly rounded grain, restrained warm ink absorption,
and a brown edge. Mask-protected ink stays smooth; artwork and lettering stay anchored.
The original dense, deeply recessed grain was rejected for poor readability.
The current trial uses moderate soft-grain relief (0.0032 scene units at default
detail), between the original harsh treatment and the overly faint first revision.
- **Gold leaf finish:** warm metallic gilding with fine irregular folds and mottling,
rather than ordinary foil's mostly color-preserving sheen.
Its gold range and highlights are compressed, with more original ink retained,
to reduce contrast without removing the folded texture.
- **Frosted glass finish:** cool satin haze and fine etched microrelief, without
transparency or blurring the underlying artwork.
Vellum and Pearlescent were removed after visual review because they were not
sufficiently distinct. Older experiments using either report an unsupported format.
All finishes follow the supplied finish mask, including text protection. Leather also
uses the mask to suppress grain over protected ink. Other material textures cover the
substrate independently of that mask. Finish strength controls
the new coatings; Surface detail controls the material relief. No card-specific
regions, artwork generation, or additional texture files are required.
Choose **Pack** in the Mode selector (expand **Controls** first on mobile).
Use **Pack style** in the same toolbar to compare three wrapper-only design studies:

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()