|
|
|
|
@@ -1,4 +1,5 @@
|
|
|
|
|
import * as THREE from 'three'
|
|
|
|
|
import { cardSceneDimensions } from './cardGeometry'
|
|
|
|
|
|
|
|
|
|
export type FinishName = 'Printed ink' | 'Foil' | 'Holographic'
|
|
|
|
|
export type SubstrateName = 'Paper' | 'Linen' | 'Plastic' | 'Metal' | 'Wood'
|
|
|
|
|
@@ -98,6 +99,7 @@ const fragmentShader = `
|
|
|
|
|
uniform float normalStrength;
|
|
|
|
|
uniform float condition;
|
|
|
|
|
uniform float imperfectionSeed;
|
|
|
|
|
uniform vec2 cardFaceSize;
|
|
|
|
|
uniform int finishMode;
|
|
|
|
|
uniform int substrateMode;
|
|
|
|
|
uniform int lightMode;
|
|
|
|
|
@@ -132,6 +134,25 @@ const fragmentShader = `
|
|
|
|
|
return sin(phase) * 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;
|
|
|
|
|
vec2 phase = uv * frequency;
|
|
|
|
|
vec2 thread = sin(phase) * visibility.xy;
|
|
|
|
|
vec2 crossing = sin(phase * 0.5) * visibility.zw;
|
|
|
|
|
float over = 0.5 + 0.5 * crossing.x * crossing.y;
|
|
|
|
|
vec2 threadSlope = cos(phase) * frequency * visibility.xy;
|
|
|
|
|
vec2 crossingSlope = cos(phase * 0.5) * frequency * 0.5 * visibility.zw;
|
|
|
|
|
vec2 overSlope = 0.5 * crossingSlope * crossing.yx;
|
|
|
|
|
vec2 slope = vec2((1.0 - over) * threadSlope.x, over * threadSlope.y)
|
|
|
|
|
+ (thread.y - thread.x) * overSlope;
|
|
|
|
|
return vec3(0.5 + 0.5 * mix(thread.x, thread.y, over), slope * 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float metalDepth(vec3 ink) {
|
|
|
|
|
return smoothstep(0.08, 0.92, 1.0 - dot(ink, vec3(0.2126, 0.7152, 0.0722)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float woodGrain(vec2 uv) {
|
|
|
|
|
float warp = fiberNoise(uv * vec2(4.0, 2.5));
|
|
|
|
|
float phase = uv.x * 170.0 + warp * 16.0 + sin(uv.y * 9.0) * 2.5;
|
|
|
|
|
@@ -208,28 +229,55 @@ const fragmentShader = `
|
|
|
|
|
vec2 artworkUv = vUv;
|
|
|
|
|
vec4 artworkSample = texture2D(artwork, artworkUv);
|
|
|
|
|
float mask = texture2D(finishMask, artworkUv).r;
|
|
|
|
|
float wearAmount = pow(clamp(1.0 - condition, 0.0, 1.0), 1.15);
|
|
|
|
|
float edgeDistance = min(
|
|
|
|
|
min(artworkUv.x, 1.0 - artworkUv.x),
|
|
|
|
|
min(artworkUv.y, 1.0 - artworkUv.y)
|
|
|
|
|
);
|
|
|
|
|
float edgeNoise = fiberNoise(artworkUv * vec2(43.0, 61.0) + imperfectionSeed * 0.0007);
|
|
|
|
|
float edgeWidth = mix(0.0015, 0.032, wearAmount) * mix(0.65, 1.25, edgeNoise);
|
|
|
|
|
float edgeWear = (1.0 - smoothstep(edgeWidth * 0.45, edgeWidth, edgeDistance)) * wearAmount;
|
|
|
|
|
float scratchWear = max(
|
|
|
|
|
scratchLine(artworkUv, 1.0),
|
|
|
|
|
max(scratchLine(artworkUv, 2.0), scratchLine(artworkUv, 3.0))
|
|
|
|
|
) * smoothstep(0.04, 0.48, wearAmount);
|
|
|
|
|
float scuffWear = max(
|
|
|
|
|
scuffMark(artworkUv, 1.0),
|
|
|
|
|
scuffMark(artworkUv, 2.0)
|
|
|
|
|
) * smoothstep(0.18, 0.82, wearAmount);
|
|
|
|
|
float metalRelief = 0.010 * min(normalStrength / 0.14, 1.5);
|
|
|
|
|
float metalBoundary = 1.0;
|
|
|
|
|
if (substrateMode == 3) {
|
|
|
|
|
vec2 edgeDistance = min(vUv, 1.0 - vUv);
|
|
|
|
|
metalBoundary = smoothstep(0.0, 0.025, min(edgeDistance.x, edgeDistance.y));
|
|
|
|
|
vec3 view = normalize(cameraPosition - vWorldPosition);
|
|
|
|
|
vec2 alongSurface = vec2(dot(view, normalize(vWorldTangent)),
|
|
|
|
|
dot(view, normalize(vWorldBitangent)));
|
|
|
|
|
vec2 ray = alongSurface / max(abs(dot(view, normalize(vWorldNormal))), 0.35)
|
|
|
|
|
* metalRelief * metalBoundary / cardFaceSize;
|
|
|
|
|
float exposure = finishMode == 1 ? 1.0 - mask * 0.25 : 1.0;
|
|
|
|
|
float depth = metalDepth(artworkSample.rgb) * exposure;
|
|
|
|
|
// A bounded predictor/corrector adds recess parallax without ray marching.
|
|
|
|
|
vec2 predictedUv = clamp(vUv - ray * depth, vec2(0.0), vec2(1.0));
|
|
|
|
|
float predictedDepth = metalDepth(texture2D(artwork, predictedUv).rgb) * exposure;
|
|
|
|
|
artworkUv = clamp(vUv - ray * (depth + predictedDepth) * 0.5, vec2(0.0), vec2(1.0));
|
|
|
|
|
artworkSample = texture2D(artwork, artworkUv);
|
|
|
|
|
mask = texture2D(finishMask, artworkUv).r;
|
|
|
|
|
}
|
|
|
|
|
float edgeWear = 0.0;
|
|
|
|
|
float scratchWear = 0.0;
|
|
|
|
|
float scuffWear = 0.0;
|
|
|
|
|
if (condition < 1.0) {
|
|
|
|
|
float wearAmount = pow(clamp(1.0 - condition, 0.0, 1.0), 1.15);
|
|
|
|
|
float edgeDistance = min(
|
|
|
|
|
min(artworkUv.x, 1.0 - artworkUv.x),
|
|
|
|
|
min(artworkUv.y, 1.0 - artworkUv.y)
|
|
|
|
|
);
|
|
|
|
|
float edgeNoise = fiberNoise(artworkUv * vec2(43.0, 61.0) + imperfectionSeed * 0.0007);
|
|
|
|
|
float edgeWidth = mix(0.0015, 0.032, wearAmount) * mix(0.65, 1.25, edgeNoise);
|
|
|
|
|
edgeWear = (1.0 - smoothstep(edgeWidth * 0.45, edgeWidth, edgeDistance)) * wearAmount;
|
|
|
|
|
scratchWear = max(
|
|
|
|
|
scratchLine(artworkUv, 1.0),
|
|
|
|
|
max(scratchLine(artworkUv, 2.0), scratchLine(artworkUv, 3.0))
|
|
|
|
|
) * smoothstep(0.04, 0.48, wearAmount);
|
|
|
|
|
scuffWear = max(
|
|
|
|
|
scuffMark(artworkUv, 1.0),
|
|
|
|
|
scuffMark(artworkUv, 2.0)
|
|
|
|
|
) * smoothstep(0.18, 0.82, wearAmount);
|
|
|
|
|
}
|
|
|
|
|
float wearDamage = max(edgeWear, max(scratchWear * 0.8, scuffWear * 0.55));
|
|
|
|
|
mask *= 1.0 - wearDamage * 0.82;
|
|
|
|
|
vec3 normalSample = texture2D(normalMap, artworkUv * 2.2).xyz * 2.0 - 1.0;
|
|
|
|
|
float detail = normalStrength / 0.14;
|
|
|
|
|
float surfaceHeight = 0.0;
|
|
|
|
|
float surfaceShade = 1.0;
|
|
|
|
|
vec2 linenSlope = vec2(0.0);
|
|
|
|
|
float linenRidge = 0.0;
|
|
|
|
|
float woodSurfaceGrain = 0.5;
|
|
|
|
|
float metalEtchDepth = 0.0;
|
|
|
|
|
float metalEtchExposure = 1.0;
|
|
|
|
|
if (substrateMode == 0) {
|
|
|
|
|
@@ -240,30 +288,45 @@ const fragmentShader = `
|
|
|
|
|
surfaceShade = 1.0 + fibers * 0.045 * min(detail, 1.5);
|
|
|
|
|
}
|
|
|
|
|
if (substrateMode == 1) {
|
|
|
|
|
vec2 thread = artworkUv * vec2(52.0, 73.0) * 6.2831853;
|
|
|
|
|
float warp = filteredWave(thread.x);
|
|
|
|
|
float weft = filteredWave(thread.y);
|
|
|
|
|
float overUnder = filteredWave(thread.x * 0.5) * filteredWave(thread.y * 0.5);
|
|
|
|
|
float weave = mix(warp, weft, 0.5 + 0.5 * overUnder);
|
|
|
|
|
surfaceHeight = weave * 0.005 * detail;
|
|
|
|
|
surfaceShade = 1.0 - (1.0 - weave) * 0.055 * min(detail, 1.5);
|
|
|
|
|
vec2 phase = artworkUv * vec2(52.0, 73.0) * 6.2831853;
|
|
|
|
|
vec2 footprint = fwidth(phase);
|
|
|
|
|
vec4 visibility = 1.0 - smoothstep(vec4(0.7), vec4(3.0),
|
|
|
|
|
vec4(footprint, footprint * 0.5));
|
|
|
|
|
float relief = 0.010 * min(detail, 1.5);
|
|
|
|
|
vec3 weave = linenRelief(artworkUv, visibility);
|
|
|
|
|
vec3 view = normalize(cameraPosition - vWorldPosition);
|
|
|
|
|
vec2 alongSurface = vec2(dot(view, normalize(vWorldTangent)),
|
|
|
|
|
dot(view, normalize(vWorldBitangent)));
|
|
|
|
|
// Offset only the weave, not the illustration, mask or printed lettering.
|
|
|
|
|
vec2 parallax = alongSurface / max(abs(dot(view, normalize(vWorldNormal))), 0.3)
|
|
|
|
|
* (weave.x - 0.5) * relief / cardFaceSize;
|
|
|
|
|
weave = linenRelief(artworkUv + parallax, visibility);
|
|
|
|
|
linenSlope = weave.yz * relief / cardFaceSize;
|
|
|
|
|
linenRidge = smoothstep(0.45, 0.9, weave.x) * min(visibility.x, visibility.y) * min(detail, 1.5);
|
|
|
|
|
surfaceShade = 1.0 - (1.0 - weave.x) * 0.11 * min(detail, 1.5);
|
|
|
|
|
}
|
|
|
|
|
if (substrateMode == 3) {
|
|
|
|
|
float artworkLuminance = dot(artworkSample.rgb, vec3(0.2126, 0.7152, 0.0722));
|
|
|
|
|
metalEtchDepth = smoothstep(0.08, 0.92, 1.0 - artworkLuminance);
|
|
|
|
|
metalEtchDepth = metalDepth(artworkSample.rgb);
|
|
|
|
|
if (finishMode == 1) {
|
|
|
|
|
metalEtchExposure = 1.0 - mask * 0.25;
|
|
|
|
|
}
|
|
|
|
|
surfaceHeight = -metalEtchDepth * metalEtchExposure * 0.0065 * min(detail, 1.5);
|
|
|
|
|
surfaceHeight = -metalEtchDepth * metalEtchExposure * metalRelief;
|
|
|
|
|
if (finishMode == 1) {
|
|
|
|
|
surfaceHeight += mask * 0.0005 * min(detail, 1.5);
|
|
|
|
|
}
|
|
|
|
|
surfaceShade = 1.0 - metalEtchDepth * metalEtchExposure * 0.035;
|
|
|
|
|
}
|
|
|
|
|
if (substrateMode == 4) {
|
|
|
|
|
float relief = 0.012 * min(detail, 1.5);
|
|
|
|
|
vec3 view = normalize(cameraPosition - vWorldPosition);
|
|
|
|
|
vec2 alongSurface = vec2(dot(view, normalize(vWorldTangent)),
|
|
|
|
|
dot(view, normalize(vWorldBitangent)));
|
|
|
|
|
float grain = woodGrain(artworkUv);
|
|
|
|
|
surfaceHeight = grain * 0.007 * detail;
|
|
|
|
|
surfaceShade = 0.88 + grain * 0.12;
|
|
|
|
|
vec2 parallax = alongSurface / max(abs(dot(view, normalize(vWorldNormal))), 0.35)
|
|
|
|
|
* (grain - 0.5) * relief / cardFaceSize;
|
|
|
|
|
woodSurfaceGrain = woodGrain(artworkUv + parallax);
|
|
|
|
|
surfaceHeight = woodSurfaceGrain * relief;
|
|
|
|
|
surfaceShade = 0.88 + woodSurfaceGrain * 0.12;
|
|
|
|
|
}
|
|
|
|
|
surfaceHeight -= scratchWear * 0.0018 + scuffWear * 0.0007;
|
|
|
|
|
float substrateNormalScale = substrateMode == 3 ? 0.06 : 0.12;
|
|
|
|
|
@@ -273,6 +336,10 @@ const fragmentShader = `
|
|
|
|
|
vWorldBitangent * normalSample.y * normalStrength * substrateNormalScale
|
|
|
|
|
);
|
|
|
|
|
normal = reliefNormal(normal, surfaceHeight);
|
|
|
|
|
if (substrateMode == 1) {
|
|
|
|
|
normal = normalize(normal - normalize(vWorldTangent) * linenSlope.x
|
|
|
|
|
- normalize(vWorldBitangent) * linenSlope.y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
|
|
|
|
|
vec3 lightVector = lightPosition - vWorldPosition;
|
|
|
|
|
@@ -316,6 +383,12 @@ const fragmentShader = `
|
|
|
|
|
float reflectionStrength = substrateMode == 1 ? 0.025 : (substrateMode == 4 ? 0.09 : 0.045);
|
|
|
|
|
printedInk += lightColor * substrateSpecular * reflectionStrength;
|
|
|
|
|
}
|
|
|
|
|
if (substrateMode == 1) {
|
|
|
|
|
float threadHighlight = pow(max(dot(normal, halfVector), 0.0), 24.0);
|
|
|
|
|
float grazing = 1.0 - max(dot(normalize(vWorldNormal), viewDirection), 0.0);
|
|
|
|
|
printedInk += lightColor * threadHighlight * diffuse * lightIntensity * attenuation
|
|
|
|
|
* linenRidge * (0.025 + 0.055 * grazing);
|
|
|
|
|
}
|
|
|
|
|
if (substrateMode == 2) {
|
|
|
|
|
float reflectedFraction = min(0.32, dielectricFresnel * environmentIntensity);
|
|
|
|
|
printedInk = printedInk * (1.0 - reflectedFraction) + environment * reflectedFraction;
|
|
|
|
|
@@ -324,6 +397,13 @@ const fragmentShader = `
|
|
|
|
|
if (substrateMode == 3) {
|
|
|
|
|
float visibleEtch = metalEtchDepth * metalEtchExposure;
|
|
|
|
|
float etchEdge = smoothstep(0.002, 0.045, fwidth(visibleEtch));
|
|
|
|
|
vec2 alongLight = vec2(dot(lightDirection, normalize(vWorldTangent)),
|
|
|
|
|
dot(lightDirection, normalize(vWorldBitangent)));
|
|
|
|
|
vec2 ridgeUv = clamp(artworkUv + alongLight * metalRelief * metalBoundary / cardFaceSize,
|
|
|
|
|
vec2(0.0), vec2(1.0));
|
|
|
|
|
float ridgeDepth = metalDepth(texture2D(artwork, ridgeUv).rgb) * metalEtchExposure;
|
|
|
|
|
float recessShadow = smoothstep(0.02, 0.25, visibleEtch - ridgeDepth)
|
|
|
|
|
* min(detail, 1.5) * metalBoundary;
|
|
|
|
|
vec3 metalUnderprint = mix(
|
|
|
|
|
artworkSample.rgb * vec3(0.72, 0.76, 0.82),
|
|
|
|
|
environment,
|
|
|
|
|
@@ -334,10 +414,10 @@ const fragmentShader = `
|
|
|
|
|
printedInk = mix(printedInk, metalUnderprint, 0.42);
|
|
|
|
|
printedInk *= 1.0 - visibleEtch * 0.06;
|
|
|
|
|
printedInk += lightColor * etchEdge * 0.025;
|
|
|
|
|
printedInk *= 1.0 - recessShadow * 0.22;
|
|
|
|
|
}
|
|
|
|
|
if (substrateMode == 4) {
|
|
|
|
|
float grain = woodGrain(artworkUv);
|
|
|
|
|
vec3 woodTint = mix(vec3(0.72, 0.40, 0.18), vec3(1.0, 0.86, 0.65), grain);
|
|
|
|
|
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 (finishMode == 0) {
|
|
|
|
|
@@ -455,6 +535,7 @@ export function createCardMaterial(
|
|
|
|
|
normalStrength: { value: 0.14 },
|
|
|
|
|
condition: { value: 1.0 },
|
|
|
|
|
imperfectionSeed: { value: 81251 },
|
|
|
|
|
cardFaceSize: { value: new THREE.Vector2(cardSceneDimensions.width, cardSceneDimensions.height) },
|
|
|
|
|
finishMode: { value: 2 },
|
|
|
|
|
substrateMode: { value: 0 },
|
|
|
|
|
lightMode: { value: 0 },
|
|
|
|
|
|