Add WebGL cloth and paper materials
This commit is contained in:
@@ -208,6 +208,7 @@ function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth, co
|
|||||||
const uAt = (x) => (x - leftX) / (rightX - leftX || 1);
|
const uAt = (x) => (x - leftX) / (rightX - leftX || 1);
|
||||||
const vAt = (z) => (z + halfDepth) / depth;
|
const vAt = (z) => (z + halfDepth) / depth;
|
||||||
const pointAt = (x, y, z) => ({ x, y, z, u: uAt(x), v: vAt(z) });
|
const pointAt = (x, y, z) => ({ x, y, z, u: uAt(x), v: vAt(z) });
|
||||||
|
const coverProfileXs = section.map((point) => point.x);
|
||||||
const edgeProfile = Array.from({ length: edgeSteps + 1 }, (_, index) => {
|
const edgeProfile = Array.from({ length: edgeSteps + 1 }, (_, index) => {
|
||||||
const angle = Math.PI * 0.5 - (index / edgeSteps) * Math.PI;
|
const angle = Math.PI * 0.5 - (index / edgeSteps) * Math.PI;
|
||||||
return {
|
return {
|
||||||
@@ -221,14 +222,24 @@ function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth, co
|
|||||||
const cornerRadius = Math.max(0.0001, edgeRadius - inset);
|
const cornerRadius = Math.max(0.0001, edgeRadius - inset);
|
||||||
const points = [];
|
const points = [];
|
||||||
const pushLinear = (fromX, fromZ, toX, toZ, steps) => {
|
const pushLinear = (fromX, fromZ, toX, toZ, steps) => {
|
||||||
for (let step = 0; step <= steps; step += 1) {
|
const candidates = [];
|
||||||
if (points.length && step === 0) continue;
|
for (let step = 0; step <= steps; step += 1) candidates.push(step / steps);
|
||||||
const t = step / steps;
|
if (Math.abs(fromZ - toZ) < 0.000001) {
|
||||||
points.push({
|
coverProfileXs.forEach((x) => {
|
||||||
x: THREE.MathUtils.lerp(fromX, toX, t),
|
const t = (x - fromX) / (toX - fromX || 1);
|
||||||
z: THREE.MathUtils.lerp(fromZ, toZ, t)
|
if (t > 0 && t < 1) candidates.push(t);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
candidates
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
.forEach((t) => {
|
||||||
|
if (points.length && Math.abs(t) < 0.000001) return;
|
||||||
|
const x = THREE.MathUtils.lerp(fromX, toX, t);
|
||||||
|
const z = THREE.MathUtils.lerp(fromZ, toZ, t);
|
||||||
|
const previous = points[points.length - 1];
|
||||||
|
if (previous && Math.hypot(previous.x - x, previous.z - z) < 0.000001) return;
|
||||||
|
points.push({ x, z });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const pushCorner = (centerX, centerZ, fromAngle, toAngle) => {
|
const pushCorner = (centerX, centerZ, fromAngle, toAngle) => {
|
||||||
for (let step = 1; step <= cornerSteps; step += 1) {
|
for (let step = 1; step <= cornerSteps; step += 1) {
|
||||||
|
|||||||
+283
-27
@@ -169,6 +169,8 @@ const rightTexture = new THREE.CanvasTexture(rightCanvas);
|
|||||||
texture.generateMipmaps = true;
|
texture.generateMipmaps = true;
|
||||||
});
|
});
|
||||||
const leatherTextures = createLeatherTextures();
|
const leatherTextures = createLeatherTextures();
|
||||||
|
const spineClothTextures = createSpineClothTextures();
|
||||||
|
const paperTextures = createHardcoverPaperTextures();
|
||||||
|
|
||||||
const materials = {
|
const materials = {
|
||||||
leather: new THREE.MeshStandardMaterial({
|
leather: new THREE.MeshStandardMaterial({
|
||||||
@@ -216,55 +218,80 @@ const materials = {
|
|||||||
side: THREE.DoubleSide
|
side: THREE.DoubleSide
|
||||||
}),
|
}),
|
||||||
pageBlock: new THREE.MeshStandardMaterial({
|
pageBlock: new THREE.MeshStandardMaterial({
|
||||||
color: 0xe3c98f,
|
color: 0xfffbef,
|
||||||
roughness: 0.82,
|
map: paperTextures.color,
|
||||||
|
normalMap: paperTextures.normal,
|
||||||
|
normalScale: new THREE.Vector2(0.032, 0.032),
|
||||||
|
roughnessMap: paperTextures.roughness,
|
||||||
|
roughness: 0.88,
|
||||||
metalness: 0,
|
metalness: 0,
|
||||||
envMapIntensity: 0.08
|
envMapIntensity: 0.06
|
||||||
}),
|
}),
|
||||||
pageEdge: new THREE.MeshStandardMaterial({
|
pageEdge: new THREE.MeshStandardMaterial({
|
||||||
color: 0xc69f64,
|
color: 0xfff4cf,
|
||||||
roughness: 0.92,
|
map: paperTextures.edge,
|
||||||
|
normalMap: paperTextures.normal,
|
||||||
|
normalScale: new THREE.Vector2(0.024, 0.024),
|
||||||
|
roughnessMap: paperTextures.roughness,
|
||||||
|
roughness: 0.94,
|
||||||
metalness: 0,
|
metalness: 0,
|
||||||
envMapIntensity: 0.08
|
envMapIntensity: 0.05
|
||||||
}),
|
}),
|
||||||
pageSurface: new THREE.MeshStandardMaterial({
|
pageSurface: new THREE.MeshStandardMaterial({
|
||||||
color: 0xf0c17a,
|
color: 0xfffbf0,
|
||||||
roughness: 0.86,
|
map: paperTextures.color,
|
||||||
|
normalMap: paperTextures.normal,
|
||||||
|
normalScale: new THREE.Vector2(0.03, 0.03),
|
||||||
|
roughnessMap: paperTextures.roughness,
|
||||||
|
roughness: 0.9,
|
||||||
metalness: 0,
|
metalness: 0,
|
||||||
emissive: 0x1e1209,
|
emissive: 0x14110b,
|
||||||
emissiveIntensity: 0.08,
|
emissiveIntensity: 0.025,
|
||||||
envMapIntensity: 0.04,
|
envMapIntensity: 0.035,
|
||||||
side: THREE.DoubleSide
|
side: THREE.DoubleSide
|
||||||
}),
|
}),
|
||||||
leftPage: new THREE.MeshStandardMaterial({
|
leftPage: new THREE.MeshStandardMaterial({
|
||||||
color: 0xffffff,
|
color: 0xffffff,
|
||||||
map: leftTexture,
|
map: leftTexture,
|
||||||
roughness: 0.74,
|
normalMap: paperTextures.normal,
|
||||||
|
normalScale: new THREE.Vector2(0.025, 0.025),
|
||||||
|
roughnessMap: paperTextures.roughness,
|
||||||
|
roughness: 0.86,
|
||||||
metalness: 0,
|
metalness: 0,
|
||||||
emissive: 0x2d1e12,
|
emissive: 0x11100c,
|
||||||
emissiveIntensity: 0.18,
|
emissiveIntensity: 0.035,
|
||||||
side: THREE.DoubleSide
|
side: THREE.DoubleSide
|
||||||
}),
|
}),
|
||||||
rightPage: new THREE.MeshStandardMaterial({
|
rightPage: new THREE.MeshStandardMaterial({
|
||||||
color: 0xffffff,
|
color: 0xffffff,
|
||||||
map: rightTexture,
|
map: rightTexture,
|
||||||
roughness: 0.74,
|
normalMap: paperTextures.normal,
|
||||||
|
normalScale: new THREE.Vector2(0.025, 0.025),
|
||||||
|
roughnessMap: paperTextures.roughness,
|
||||||
|
roughness: 0.86,
|
||||||
metalness: 0,
|
metalness: 0,
|
||||||
emissive: 0x2d1e12,
|
emissive: 0x11100c,
|
||||||
emissiveIntensity: 0.18,
|
emissiveIntensity: 0.035,
|
||||||
side: THREE.DoubleSide
|
side: THREE.DoubleSide
|
||||||
}),
|
}),
|
||||||
spineCloth: new THREE.MeshStandardMaterial({
|
spineCloth: new THREE.MeshStandardMaterial({
|
||||||
color: 0x8e1d18,
|
color: 0x6f0808,
|
||||||
normalMap: leatherTextures.normal,
|
map: spineClothTextures.color,
|
||||||
normalScale: new THREE.Vector2(0.04, 0.04),
|
normalMap: spineClothTextures.normal,
|
||||||
roughnessMap: leatherTextures.roughness,
|
normalScale: new THREE.Vector2(0.075, 0.075),
|
||||||
roughness: 0.82,
|
roughnessMap: spineClothTextures.roughness,
|
||||||
|
roughness: 0.9,
|
||||||
metalness: 0,
|
metalness: 0,
|
||||||
envMapIntensity: 0.08,
|
envMapIntensity: 0.045,
|
||||||
side: THREE.DoubleSide
|
side: THREE.DoubleSide
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
materials.spineCloth.userData.isSpineCloth = true;
|
||||||
|
configureHardcoverPaperMaterial(materials.pageBlock);
|
||||||
|
configureHardcoverPaperMaterial(materials.pageEdge, { useEdgeMap: true });
|
||||||
|
configureHardcoverPaperMaterial(materials.pageSurface);
|
||||||
|
configureHardcoverPaperMaterial(materials.leftPage);
|
||||||
|
configureHardcoverPaperMaterial(materials.rightPage);
|
||||||
|
|
||||||
configureBookShadowReceiver(materials.leather, 0.52);
|
configureBookShadowReceiver(materials.leather, 0.52);
|
||||||
configureBookShadowReceiver(materials.hingeLeather, 0.36);
|
configureBookShadowReceiver(materials.hingeLeather, 0.36);
|
||||||
@@ -393,7 +420,9 @@ function loadUtilityTexture(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function configureBookShadowReceiver(material, strength) {
|
function configureBookShadowReceiver(material, strength) {
|
||||||
material.customProgramCacheKey = () => `book-shadow-receiver-${strength.toFixed(2)}`;
|
const isSpineCloth = material.userData?.isSpineCloth === true;
|
||||||
|
const isHardcoverPaper = material.userData?.isHardcoverPaper === true;
|
||||||
|
material.customProgramCacheKey = () => `book-shadow-receiver-${strength.toFixed(2)}-${isSpineCloth ? 'spine-cloth-v1' : isHardcoverPaper ? 'hardcover-paper-v1' : 'plain'}`;
|
||||||
material.onBeforeCompile = (shader) => {
|
material.onBeforeCompile = (shader) => {
|
||||||
shader.uniforms.bookShadowMaps = { value: bookShadowTargets.map((target) => target.texture) };
|
shader.uniforms.bookShadowMaps = { value: bookShadowTargets.map((target) => target.texture) };
|
||||||
shader.uniforms.bookShadowMatrices = { value: bookShadowMatrices };
|
shader.uniforms.bookShadowMatrices = { value: bookShadowMatrices };
|
||||||
@@ -403,7 +432,14 @@ function configureBookShadowReceiver(material, strength) {
|
|||||||
shader.vertexShader = shader.vertexShader
|
shader.vertexShader = shader.vertexShader
|
||||||
.replace(
|
.replace(
|
||||||
'#include <common>',
|
'#include <common>',
|
||||||
'#include <common>\nvarying vec3 vBookReceiverWorldPosition;'
|
`#include <common>
|
||||||
|
varying vec3 vBookReceiverWorldPosition;
|
||||||
|
${isSpineCloth || isHardcoverPaper ? 'varying vec2 vBookSurfaceUv;' : ''}`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
'#include <begin_vertex>',
|
||||||
|
`${isSpineCloth || isHardcoverPaper ? 'vBookSurfaceUv = uv;' : ''}
|
||||||
|
#include <begin_vertex>`
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
'#include <project_vertex>',
|
'#include <project_vertex>',
|
||||||
@@ -419,6 +455,7 @@ function configureBookShadowReceiver(material, strength) {
|
|||||||
uniform vec2 bookShadowMapTexelSize;
|
uniform vec2 bookShadowMapTexelSize;
|
||||||
uniform float bookShadowReceiverStrength;
|
uniform float bookShadowReceiverStrength;
|
||||||
varying vec3 vBookReceiverWorldPosition;
|
varying vec3 vBookReceiverWorldPosition;
|
||||||
|
${isSpineCloth || isHardcoverPaper ? 'varying vec2 vBookSurfaceUv;' : ''}
|
||||||
|
|
||||||
float bookReceiverUnpackRGBADepth(vec4 packedDepth) {
|
float bookReceiverUnpackRGBADepth(vec4 packedDepth) {
|
||||||
const vec4 unpackFactors = vec4(
|
const vec4 unpackFactors = vec4(
|
||||||
@@ -485,11 +522,45 @@ function configureBookShadowReceiver(material, strength) {
|
|||||||
float shadow1 = bookReceiverSample1(bookShadowMatrices[1] * vec4(worldPosition, 1.0));
|
float shadow1 = bookReceiverSample1(bookShadowMatrices[1] * vec4(worldPosition, 1.0));
|
||||||
float shadow2 = bookReceiverSample2(bookShadowMatrices[2] * vec4(worldPosition, 1.0));
|
float shadow2 = bookReceiverSample2(bookShadowMatrices[2] * vec4(worldPosition, 1.0));
|
||||||
return clamp(max(max(shadow0, shadow1), shadow2), 0.0, 1.0);
|
return clamp(max(max(shadow0, shadow1), shadow2), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float spineClothThread(float coordinate, float frequency, float sharpness) {
|
||||||
|
float wave = abs(fract(coordinate * frequency) - 0.5) * 2.0;
|
||||||
|
return pow(1.0 - wave, sharpness);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 spineClothLight(vec2 uv, vec3 baseLight) {
|
||||||
|
float warp = spineClothThread(uv.x + sin(uv.y * 18.0) * 0.002, 92.0, 2.4);
|
||||||
|
float weft = spineClothThread(uv.y + sin(uv.x * 21.0) * 0.0016, 64.0, 2.1);
|
||||||
|
float fineFiber = sin((uv.x * 420.0 + uv.y * 55.0) * 6.28318530718) *
|
||||||
|
sin((uv.y * 380.0 - uv.x * 33.0) * 6.28318530718);
|
||||||
|
float raisedThread = clamp(warp * 0.58 + weft * 0.44, 0.0, 1.0);
|
||||||
|
float valley = clamp((1.0 - warp) * (1.0 - weft), 0.0, 1.0);
|
||||||
|
vec3 threadTint = mix(vec3(0.55, 0.19, 0.16), vec3(1.18, 0.78, 0.58), raisedThread);
|
||||||
|
float fiberShade = 0.9 + fineFiber * 0.035 - valley * 0.18;
|
||||||
|
return baseLight * threadTint * fiberShade;
|
||||||
|
}
|
||||||
|
|
||||||
|
float paperFiber(float coordinate, float frequency, float sharpness) {
|
||||||
|
float wave = abs(fract(coordinate * frequency) - 0.5) * 2.0;
|
||||||
|
return pow(1.0 - wave, sharpness);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hardcoverPaperLight(vec2 uv, vec3 baseLight) {
|
||||||
|
float laid = paperFiber(uv.y + sin(uv.x * 12.0) * 0.002, 88.0, 2.8);
|
||||||
|
float chain = paperFiber(uv.x + sin(uv.y * 8.0) * 0.0015, 18.0, 1.6);
|
||||||
|
float fleck = sin((uv.x * 241.0 + uv.y * 97.0) * 6.28318530718) *
|
||||||
|
sin((uv.y * 211.0 - uv.x * 53.0) * 6.28318530718);
|
||||||
|
float fiber = clamp(laid * 0.18 + chain * 0.1 + fleck * 0.025, -0.08, 0.24);
|
||||||
|
vec3 paperTint = mix(vec3(0.92, 0.9, 0.82), vec3(1.12, 1.08, 0.96), clamp(0.58 + fiber, 0.0, 1.0));
|
||||||
|
return baseLight * paperTint;
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
'#include <opaque_fragment>',
|
'#include <opaque_fragment>',
|
||||||
`float bookReceiverShadow = bookReceiverShadowField(vBookReceiverWorldPosition) * bookShadowReceiverStrength;
|
`${isSpineCloth ? 'outgoingLight = spineClothLight(vBookSurfaceUv, outgoingLight);' : ''}
|
||||||
|
${isHardcoverPaper ? 'outgoingLight = hardcoverPaperLight(vBookSurfaceUv, outgoingLight);' : ''}
|
||||||
|
float bookReceiverShadow = bookReceiverShadowField(vBookReceiverWorldPosition) * bookShadowReceiverStrength;
|
||||||
outgoingLight *= mix(vec3(1.0), vec3(0.38, 0.29, 0.2), bookReceiverShadow);
|
outgoingLight *= mix(vec3(1.0), vec3(0.38, 0.29, 0.2), bookReceiverShadow);
|
||||||
#include <opaque_fragment>`
|
#include <opaque_fragment>`
|
||||||
);
|
);
|
||||||
@@ -1190,6 +1261,9 @@ function buildBook() {
|
|||||||
rightPage: materials.rightPage
|
rightPage: materials.rightPage
|
||||||
},
|
},
|
||||||
configureMaterial(material, part) {
|
configureMaterial(material, part) {
|
||||||
|
if (part === 'pages') {
|
||||||
|
configureHardcoverPaperMaterial(material, { useEdgeMap: material.map !== null });
|
||||||
|
}
|
||||||
const strength = part === 'spine'
|
const strength = part === 'spine'
|
||||||
? 0.48
|
? 0.48
|
||||||
: part === 'coverSpineBase'
|
: part === 'coverSpineBase'
|
||||||
@@ -1208,6 +1282,18 @@ function buildBook() {
|
|||||||
book.add(proceduralBook.group);
|
book.add(proceduralBook.group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function configureHardcoverPaperMaterial(material, { useEdgeMap = false } = {}) {
|
||||||
|
material.userData.isHardcoverPaper = true;
|
||||||
|
if (!material.map) material.map = useEdgeMap ? paperTextures.edge : paperTextures.color;
|
||||||
|
material.normalMap = paperTextures.normal;
|
||||||
|
material.normalScale = material.normalScale ?? new THREE.Vector2(0.024, 0.024);
|
||||||
|
material.roughnessMap = paperTextures.roughness;
|
||||||
|
material.roughness = Math.max(material.roughness ?? 0.86, useEdgeMap ? 0.92 : 0.86);
|
||||||
|
material.metalness = 0;
|
||||||
|
material.envMapIntensity = Math.min(material.envMapIntensity ?? 0.05, 0.06);
|
||||||
|
material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
function setReadingProgress(value) {
|
function setReadingProgress(value) {
|
||||||
const nextProgress = THREE.MathUtils.clamp(Number.parseFloat(value), 0, 1);
|
const nextProgress = THREE.MathUtils.clamp(Number.parseFloat(value), 0, 1);
|
||||||
if (!Number.isFinite(nextProgress)) return;
|
if (!Number.isFinite(nextProgress)) return;
|
||||||
@@ -1797,7 +1883,7 @@ function createPageCanvas(side) {
|
|||||||
canvas.width = pageTextureWidth;
|
canvas.width = pageTextureWidth;
|
||||||
canvas.height = Math.round(pageTextureWidth * PROCEDURAL_BOOK.PAGE_DEPTH / PROCEDURAL_BOOK.PAGE_WIDTH);
|
canvas.height = Math.round(pageTextureWidth * PROCEDURAL_BOOK.PAGE_DEPTH / PROCEDURAL_BOOK.PAGE_WIDTH);
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.fillStyle = '#f5dfab';
|
ctx.fillStyle = '#fffaf0';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
canvas.style.width = `${canvas.width}px`;
|
canvas.style.width = `${canvas.width}px`;
|
||||||
canvas.style.height = `${canvas.height}px`;
|
canvas.style.height = `${canvas.height}px`;
|
||||||
@@ -1928,6 +2014,176 @@ function createLeatherTextures() {
|
|||||||
return { color: colorTexture, normal: normalTexture, roughness: roughnessTexture };
|
return { color: colorTexture, normal: normalTexture, roughness: roughnessTexture };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSpineClothTextures() {
|
||||||
|
const size = 1024;
|
||||||
|
const colorCanvas = document.createElement('canvas');
|
||||||
|
const normalCanvas = document.createElement('canvas');
|
||||||
|
const roughnessCanvas = document.createElement('canvas');
|
||||||
|
colorCanvas.width = size;
|
||||||
|
colorCanvas.height = size;
|
||||||
|
normalCanvas.width = size;
|
||||||
|
normalCanvas.height = size;
|
||||||
|
roughnessCanvas.width = size;
|
||||||
|
roughnessCanvas.height = size;
|
||||||
|
const colorContext = colorCanvas.getContext('2d');
|
||||||
|
const normalContext = normalCanvas.getContext('2d');
|
||||||
|
const roughnessContext = roughnessCanvas.getContext('2d');
|
||||||
|
const colorImage = colorContext.createImageData(size, size);
|
||||||
|
const normalImage = normalContext.createImageData(size, size);
|
||||||
|
const roughnessImage = roughnessContext.createImageData(size, size);
|
||||||
|
const threadAt = (x, y) => {
|
||||||
|
const nx = x / size;
|
||||||
|
const ny = y / size;
|
||||||
|
const warpPhase = nx * 112 + Math.sin(ny * 31.4159265359) * 0.025;
|
||||||
|
const weftPhase = ny * 76 + Math.sin(nx * 25.1327412287) * 0.02;
|
||||||
|
const warp = Math.pow(1 - Math.abs((warpPhase - Math.floor(warpPhase)) - 0.5) * 2, 2.2);
|
||||||
|
const weft = Math.pow(1 - Math.abs((weftPhase - Math.floor(weftPhase)) - 0.5) * 2, 2.0);
|
||||||
|
const fiber = Math.sin((nx * 430 + ny * 73) * 6.28318530718) * Math.sin((ny * 390 - nx * 41) * 6.28318530718);
|
||||||
|
const nap = Math.sin((nx * 19 + ny * 7) * 6.28318530718);
|
||||||
|
return warp * 0.46 + weft * 0.38 + fiber * 0.045 + nap * 0.055;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let y = 0; y < size; y += 1) {
|
||||||
|
for (let x = 0; x < size; x += 1) {
|
||||||
|
const index = (y * size + x) * 4;
|
||||||
|
const height = threadAt(x, y);
|
||||||
|
const wornFiber = 0.86 + 0.1 * Math.sin((x * 0.019 + y * 0.037)) + 0.04 * Math.sin((x * 0.083 - y * 0.011));
|
||||||
|
const threadGlow = THREE.MathUtils.clamp(0.58 + height * 0.46, 0, 1);
|
||||||
|
colorImage.data[index] = Math.round(95 * threadGlow * wornFiber);
|
||||||
|
colorImage.data[index + 1] = Math.round(10 * threadGlow * wornFiber);
|
||||||
|
colorImage.data[index + 2] = Math.round(9 * (0.84 + height * 0.12));
|
||||||
|
colorImage.data[index + 3] = 255;
|
||||||
|
|
||||||
|
const hLeft = threadAt((x - 1 + size) % size, y);
|
||||||
|
const hRight = threadAt((x + 1) % size, y);
|
||||||
|
const hDown = threadAt(x, (y - 1 + size) % size);
|
||||||
|
const hUp = threadAt(x, (y + 1) % size);
|
||||||
|
const normal = new THREE.Vector3((hLeft - hRight) * 5.4, (hDown - hUp) * 5.4, 1).normalize();
|
||||||
|
normalImage.data[index] = Math.round((normal.x * 0.5 + 0.5) * 255);
|
||||||
|
normalImage.data[index + 1] = Math.round((normal.y * 0.5 + 0.5) * 255);
|
||||||
|
normalImage.data[index + 2] = Math.round((normal.z * 0.5 + 0.5) * 255);
|
||||||
|
normalImage.data[index + 3] = 255;
|
||||||
|
|
||||||
|
const fiberContrast = Math.abs(hLeft - hRight) + Math.abs(hDown - hUp);
|
||||||
|
const roughness = THREE.MathUtils.clamp(0.84 + height * 0.07 + fiberContrast * 1.25, 0.62, 0.98);
|
||||||
|
const roughnessByte = Math.round(roughness * 255);
|
||||||
|
roughnessImage.data[index] = roughnessByte;
|
||||||
|
roughnessImage.data[index + 1] = roughnessByte;
|
||||||
|
roughnessImage.data[index + 2] = roughnessByte;
|
||||||
|
roughnessImage.data[index + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
colorContext.putImageData(colorImage, 0, 0);
|
||||||
|
normalContext.putImageData(normalImage, 0, 0);
|
||||||
|
roughnessContext.putImageData(roughnessImage, 0, 0);
|
||||||
|
const colorTexture = new THREE.CanvasTexture(colorCanvas);
|
||||||
|
const normalTexture = new THREE.CanvasTexture(normalCanvas);
|
||||||
|
const roughnessTexture = new THREE.CanvasTexture(roughnessCanvas);
|
||||||
|
[colorTexture, normalTexture, roughnessTexture].forEach((texture) => {
|
||||||
|
texture.wrapS = THREE.RepeatWrapping;
|
||||||
|
texture.wrapT = THREE.RepeatWrapping;
|
||||||
|
texture.repeat.set(2.1, 4.4);
|
||||||
|
texture.anisotropy = maxTextureAnisotropy;
|
||||||
|
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||||
|
texture.magFilter = THREE.LinearFilter;
|
||||||
|
texture.generateMipmaps = true;
|
||||||
|
});
|
||||||
|
colorTexture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
normalTexture.colorSpace = THREE.NoColorSpace;
|
||||||
|
roughnessTexture.colorSpace = THREE.NoColorSpace;
|
||||||
|
return { color: colorTexture, normal: normalTexture, roughness: roughnessTexture };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHardcoverPaperTextures() {
|
||||||
|
const size = 1024;
|
||||||
|
const colorCanvas = document.createElement('canvas');
|
||||||
|
const edgeCanvas = document.createElement('canvas');
|
||||||
|
const normalCanvas = document.createElement('canvas');
|
||||||
|
const roughnessCanvas = document.createElement('canvas');
|
||||||
|
[colorCanvas, edgeCanvas, normalCanvas, roughnessCanvas].forEach((canvas) => {
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
});
|
||||||
|
const colorContext = colorCanvas.getContext('2d');
|
||||||
|
const edgeContext = edgeCanvas.getContext('2d');
|
||||||
|
const normalContext = normalCanvas.getContext('2d');
|
||||||
|
const roughnessContext = roughnessCanvas.getContext('2d');
|
||||||
|
const colorImage = colorContext.createImageData(size, size);
|
||||||
|
const edgeImage = edgeContext.createImageData(size, size);
|
||||||
|
const normalImage = normalContext.createImageData(size, size);
|
||||||
|
const roughnessImage = roughnessContext.createImageData(size, size);
|
||||||
|
const fiberAt = (x, y) => {
|
||||||
|
const nx = x / size;
|
||||||
|
const ny = y / size;
|
||||||
|
const laid = Math.sin((ny * 92 + Math.sin(nx * 25.1327412287) * 0.12) * 6.28318530718);
|
||||||
|
const chain = Math.sin((nx * 18 + Math.sin(ny * 12.5663706144) * 0.06) * 6.28318530718);
|
||||||
|
const pulpA = Math.sin((nx * 173 + ny * 67) * 6.28318530718);
|
||||||
|
const pulpB = Math.sin((nx * 89 - ny * 131) * 6.28318530718);
|
||||||
|
const fleck = Math.max(0, 0.5 - Math.abs(pulpA * pulpB));
|
||||||
|
return laid * 0.08 + chain * 0.045 - fleck * 0.055;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let y = 0; y < size; y += 1) {
|
||||||
|
for (let x = 0; x < size; x += 1) {
|
||||||
|
const index = (y * size + x) * 4;
|
||||||
|
const fiber = fiberAt(x, y);
|
||||||
|
const warmth = 0.97 + 0.018 * Math.sin(x * 0.017 + y * 0.003) + 0.012 * Math.sin(y * 0.041);
|
||||||
|
const shade = THREE.MathUtils.clamp(0.975 + fiber, 0.88, 1.0);
|
||||||
|
colorImage.data[index] = Math.round(255 * shade * warmth);
|
||||||
|
colorImage.data[index + 1] = Math.round(251 * shade * warmth);
|
||||||
|
colorImage.data[index + 2] = Math.round(235 * shade);
|
||||||
|
colorImage.data[index + 3] = 255;
|
||||||
|
|
||||||
|
const line = y % 34 === 0 ? 0.72 : y % 34 === 1 ? 0.82 : 1;
|
||||||
|
edgeImage.data[index] = Math.round(255 * shade * line);
|
||||||
|
edgeImage.data[index + 1] = Math.round(244 * shade * line);
|
||||||
|
edgeImage.data[index + 2] = Math.round(207 * shade * line);
|
||||||
|
edgeImage.data[index + 3] = 255;
|
||||||
|
|
||||||
|
const hLeft = fiberAt((x - 1 + size) % size, y);
|
||||||
|
const hRight = fiberAt((x + 1) % size, y);
|
||||||
|
const hDown = fiberAt(x, (y - 1 + size) % size);
|
||||||
|
const hUp = fiberAt(x, (y + 1) % size);
|
||||||
|
const normal = new THREE.Vector3((hLeft - hRight) * 3.2, (hDown - hUp) * 3.2, 1).normalize();
|
||||||
|
normalImage.data[index] = Math.round((normal.x * 0.5 + 0.5) * 255);
|
||||||
|
normalImage.data[index + 1] = Math.round((normal.y * 0.5 + 0.5) * 255);
|
||||||
|
normalImage.data[index + 2] = Math.round((normal.z * 0.5 + 0.5) * 255);
|
||||||
|
normalImage.data[index + 3] = 255;
|
||||||
|
|
||||||
|
const roughness = THREE.MathUtils.clamp(0.86 + Math.abs(fiber) * 0.5 + Math.abs(hLeft - hRight + hDown - hUp) * 1.2, 0.72, 0.98);
|
||||||
|
const roughnessByte = Math.round(roughness * 255);
|
||||||
|
roughnessImage.data[index] = roughnessByte;
|
||||||
|
roughnessImage.data[index + 1] = roughnessByte;
|
||||||
|
roughnessImage.data[index + 2] = roughnessByte;
|
||||||
|
roughnessImage.data[index + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
colorContext.putImageData(colorImage, 0, 0);
|
||||||
|
edgeContext.putImageData(edgeImage, 0, 0);
|
||||||
|
normalContext.putImageData(normalImage, 0, 0);
|
||||||
|
roughnessContext.putImageData(roughnessImage, 0, 0);
|
||||||
|
const colorTexture = new THREE.CanvasTexture(colorCanvas);
|
||||||
|
const edgeTexture = new THREE.CanvasTexture(edgeCanvas);
|
||||||
|
const normalTexture = new THREE.CanvasTexture(normalCanvas);
|
||||||
|
const roughnessTexture = new THREE.CanvasTexture(roughnessCanvas);
|
||||||
|
[colorTexture, edgeTexture, normalTexture, roughnessTexture].forEach((texture) => {
|
||||||
|
texture.wrapS = THREE.RepeatWrapping;
|
||||||
|
texture.wrapT = THREE.RepeatWrapping;
|
||||||
|
texture.repeat.set(2.6, 3.4);
|
||||||
|
texture.anisotropy = maxTextureAnisotropy;
|
||||||
|
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||||
|
texture.magFilter = THREE.LinearFilter;
|
||||||
|
texture.generateMipmaps = true;
|
||||||
|
});
|
||||||
|
colorTexture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
edgeTexture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
normalTexture.colorSpace = THREE.NoColorSpace;
|
||||||
|
roughnessTexture.colorSpace = THREE.NoColorSpace;
|
||||||
|
return { color: colorTexture, edge: edgeTexture, normal: normalTexture, roughness: roughnessTexture };
|
||||||
|
}
|
||||||
|
|
||||||
function createRoomReflectionTexture() {
|
function createRoomReflectionTexture() {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
generatedTextureCanvases.roomReflection = canvas;
|
generatedTextureCanvases.roomReflection = canvas;
|
||||||
|
|||||||
Reference in New Issue
Block a user