Refine WebGL table surface contamination
This commit is contained in:
+134
-26
@@ -10,7 +10,8 @@ const tableDebugModes = {
|
|||||||
room: 4,
|
room: 4,
|
||||||
scene: 5,
|
scene: 5,
|
||||||
mask: 6,
|
mask: 6,
|
||||||
ao: 7
|
ao: 7,
|
||||||
|
grease: 8
|
||||||
};
|
};
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const tableDebugName = urlParams.get('tableDebug') || 'none';
|
const tableDebugName = urlParams.get('tableDebug') || 'none';
|
||||||
@@ -40,6 +41,7 @@ let tableMesh = null;
|
|||||||
let tableShader = null;
|
let tableShader = null;
|
||||||
let tableRoomReflectionTexture = createRoomReflectionTexture();
|
let tableRoomReflectionTexture = createRoomReflectionTexture();
|
||||||
let tableDustTexture = null;
|
let tableDustTexture = null;
|
||||||
|
let tableGreaseTexture = null;
|
||||||
const tableTopY = -0.02;
|
const tableTopY = -0.02;
|
||||||
const tableReflectionTarget = new THREE.WebGLRenderTarget(4096, 2304, {
|
const tableReflectionTarget = new THREE.WebGLRenderTarget(4096, 2304, {
|
||||||
colorSpace: THREE.SRGBColorSpace,
|
colorSpace: THREE.SRGBColorSpace,
|
||||||
@@ -183,17 +185,21 @@ function buildTable() {
|
|||||||
tableDustTexture.wrapS = THREE.ClampToEdgeWrapping;
|
tableDustTexture.wrapS = THREE.ClampToEdgeWrapping;
|
||||||
tableDustTexture.wrapT = THREE.ClampToEdgeWrapping;
|
tableDustTexture.wrapT = THREE.ClampToEdgeWrapping;
|
||||||
tableDustTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
tableDustTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||||
|
tableGreaseTexture = createTableGreaseTexture();
|
||||||
|
tableGreaseTexture.wrapS = THREE.ClampToEdgeWrapping;
|
||||||
|
tableGreaseTexture.wrapT = THREE.ClampToEdgeWrapping;
|
||||||
|
tableGreaseTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||||
|
|
||||||
const tableMaterial = new THREE.MeshPhysicalMaterial({
|
const tableMaterial = new THREE.MeshPhysicalMaterial({
|
||||||
color: 0x8a4c22,
|
color: 0x8a4c22,
|
||||||
map: tableTexture,
|
map: tableTexture,
|
||||||
normalMap: tableNormal,
|
normalMap: tableNormal,
|
||||||
normalScale: new THREE.Vector2(0.22, 0.18),
|
normalScale: new THREE.Vector2(0.22, 0.18),
|
||||||
roughness: 0.38,
|
roughness: 0.42,
|
||||||
metalness: 0,
|
metalness: 0,
|
||||||
clearcoat: 0.54,
|
clearcoat: 0.32,
|
||||||
clearcoatRoughness: 0.48,
|
clearcoatRoughness: 0.58,
|
||||||
reflectivity: 0.34,
|
reflectivity: 0.18,
|
||||||
envMapIntensity: 0
|
envMapIntensity: 0
|
||||||
});
|
});
|
||||||
configureTableShader(tableMaterial);
|
configureTableShader(tableMaterial);
|
||||||
@@ -510,6 +516,7 @@ function configureTableShader(material) {
|
|||||||
shader.uniforms.sceneReflectionMap = { value: tableReflectionTarget.texture };
|
shader.uniforms.sceneReflectionMap = { value: tableReflectionTarget.texture };
|
||||||
shader.uniforms.sceneReflectionMatrix = { value: tableReflectionMatrix };
|
shader.uniforms.sceneReflectionMatrix = { value: tableReflectionMatrix };
|
||||||
shader.uniforms.tableDustMap = { value: tableDustTexture };
|
shader.uniforms.tableDustMap = { value: tableDustTexture };
|
||||||
|
shader.uniforms.tableGreaseMap = { value: tableGreaseTexture };
|
||||||
shader.uniforms.candleBodyPositions = {
|
shader.uniforms.candleBodyPositions = {
|
||||||
value: [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]
|
value: [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]
|
||||||
};
|
};
|
||||||
@@ -538,6 +545,7 @@ function configureTableShader(material) {
|
|||||||
uniform sampler2D roomReflectionMap;
|
uniform sampler2D roomReflectionMap;
|
||||||
uniform sampler2D sceneReflectionMap;
|
uniform sampler2D sceneReflectionMap;
|
||||||
uniform sampler2D tableDustMap;
|
uniform sampler2D tableDustMap;
|
||||||
|
uniform sampler2D tableGreaseMap;
|
||||||
uniform mat4 sceneReflectionMatrix;
|
uniform mat4 sceneReflectionMatrix;
|
||||||
uniform vec3 candleBodyPositions[3];
|
uniform vec3 candleBodyPositions[3];
|
||||||
uniform vec3 candleFlamePositions[3];
|
uniform vec3 candleFlamePositions[3];
|
||||||
@@ -546,6 +554,22 @@ function configureTableShader(material) {
|
|||||||
varying vec3 vTableWorldPosition;
|
varying vec3 vTableWorldPosition;
|
||||||
varying vec4 vSceneReflectionCoord;
|
varying vec4 vSceneReflectionCoord;
|
||||||
|
|
||||||
|
vec2 tableDustUvFromWorld(vec3 worldPosition) {
|
||||||
|
return clamp(vec2(worldPosition.x / 9.8 + 0.5, 0.5 - worldPosition.z / 6.6), vec2(0.0), vec2(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
float tableTopMaskFromWorld(vec3 worldPosition) {
|
||||||
|
return smoothstep(-0.095, -0.025, worldPosition.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float tableDustFromWorld(vec3 worldPosition) {
|
||||||
|
return texture2D(tableDustMap, tableDustUvFromWorld(worldPosition)).r * tableTopMaskFromWorld(worldPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
float tableGreaseFromWorld(vec3 worldPosition) {
|
||||||
|
return texture2D(tableGreaseMap, tableDustUvFromWorld(worldPosition)).r * tableTopMaskFromWorld(worldPosition);
|
||||||
|
}
|
||||||
|
|
||||||
vec3 rotateRoomReflection(vec3 dir) {
|
vec3 rotateRoomReflection(vec3 dir) {
|
||||||
const float yaw = 0.42;
|
const float yaw = 0.42;
|
||||||
float s = sin(yaw);
|
float s = sin(yaw);
|
||||||
@@ -635,6 +659,16 @@ function configureTableShader(material) {
|
|||||||
return clamp(projectedShadow, 0.0, 0.46);
|
return clamp(projectedShadow, 0.0, 0.46);
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
.replace(
|
||||||
|
'#include <roughnessmap_fragment>',
|
||||||
|
`#include <roughnessmap_fragment>
|
||||||
|
float tableSpecularGrease = tableGreaseFromWorld(vTableWorldPosition);
|
||||||
|
float tableSpecularDust = tableDustFromWorld(vTableWorldPosition) * (1.0 - smoothstep(0.025, 0.18, tableSpecularGrease) * 0.82);
|
||||||
|
float tableSpecularDustFilm = smoothstep(0.006, 0.034, tableSpecularDust);
|
||||||
|
float tableSpecularGreaseFilm = smoothstep(0.016, 0.14, tableSpecularGrease);
|
||||||
|
roughnessFactor = clamp(mix(roughnessFactor, 0.84, tableSpecularDustFilm * 0.32), 0.04, 1.0);
|
||||||
|
roughnessFactor = clamp(mix(roughnessFactor, 0.34, tableSpecularGreaseFilm * 0.3), 0.04, 1.0);`
|
||||||
|
)
|
||||||
.replace(
|
.replace(
|
||||||
'#include <opaque_fragment>',
|
'#include <opaque_fragment>',
|
||||||
`vec3 viewDirWorld = normalize(cameraPosition - vTableWorldPosition);
|
`vec3 viewDirWorld = normalize(cameraPosition - vTableWorldPosition);
|
||||||
@@ -652,23 +686,32 @@ function configureTableShader(material) {
|
|||||||
smoothstep(0.0, 0.08, 1.0 - sceneReflectionUv.y);
|
smoothstep(0.0, 0.08, 1.0 - sceneReflectionUv.y);
|
||||||
vec3 sceneReflection = texture2D(sceneReflectionMap, sceneReflectionUv).rgb;
|
vec3 sceneReflection = texture2D(sceneReflectionMap, sceneReflectionUv).rgb;
|
||||||
sceneReflection = pow(max(sceneReflection, vec3(0.0)), vec3(0.88)) * sceneReflectionInBounds * sceneReflectionEdge;
|
sceneReflection = pow(max(sceneReflection, vec3(0.0)), vec3(0.88)) * sceneReflectionInBounds * sceneReflectionEdge;
|
||||||
vec2 tableDustUv = clamp(vec2(vTableWorldPosition.x / 9.8 + 0.5, 0.5 - vTableWorldPosition.z / 6.6), vec2(0.0), vec2(1.0));
|
float grease = tableGreaseFromWorld(vTableWorldPosition);
|
||||||
float dust = texture2D(tableDustMap, tableDustUv).r;
|
float greaseDustWipe = smoothstep(0.016, 0.13, grease);
|
||||||
float reflectionCleanliness = 1.0 - dust * 0.62;
|
float dust = tableDustFromWorld(vTableWorldPosition) * (1.0 - greaseDustWipe * 0.82);
|
||||||
vec3 combinedReflection = (roomReflection * 0.18 + sceneReflection * 0.5) * reflectionCleanliness;
|
float dustFilm = smoothstep(0.006, 0.034, dust);
|
||||||
|
float greaseFilm = smoothstep(0.016, 0.14, grease);
|
||||||
|
float reflectionCleanliness = 1.0 - dustFilm * 0.2 - greaseFilm * 0.06;
|
||||||
|
vec3 dustBlurredSceneReflection = sceneReflection * 0.78 + roomReflection * 0.055;
|
||||||
|
vec3 greaseBlurredSceneReflection = sceneReflection * 0.7 + roomReflection * 0.18;
|
||||||
|
vec3 dustAwareSceneReflection = mix(sceneReflection, dustBlurredSceneReflection, dustFilm);
|
||||||
|
dustAwareSceneReflection = mix(dustAwareSceneReflection, greaseBlurredSceneReflection, greaseFilm);
|
||||||
|
vec3 combinedReflection = (roomReflection * 0.14 + dustAwareSceneReflection * 0.86) * reflectionCleanliness;
|
||||||
float fresnel = pow(1.0 - max(dot(viewDirWorld, tableNormalWorld), 0.0), 1.85);
|
float fresnel = pow(1.0 - max(dot(viewDirWorld, tableNormalWorld), 0.0), 1.85);
|
||||||
float tableReflectionMask = smoothstep(-0.095, -0.025, vTableWorldPosition.y);
|
float tableReflectionMask = smoothstep(-0.095, -0.025, vTableWorldPosition.y);
|
||||||
vec3 reflectedSurface = combinedReflection * (0.56 + fresnel * 0.44);
|
vec3 reflectedSurface = combinedReflection * (0.64 + fresnel * 0.36);
|
||||||
|
float reflectedLuma = dot(reflectedSurface, vec3(0.299, 0.587, 0.114));
|
||||||
|
reflectedSurface = mix(reflectedSurface, vec3(reflectedLuma), dustFilm * 0.42);
|
||||||
float contactAo = 1.0 - smoothstep(0.0, 0.9, length(vTableWorldPosition.xz * vec2(0.34, 0.58))) * 0.16;
|
float contactAo = 1.0 - smoothstep(0.0, 0.9, length(vTableWorldPosition.xz * vec2(0.34, 0.58))) * 0.16;
|
||||||
float dustDulling = dust * tableReflectionMask;
|
|
||||||
float candleContact = candleContactField(vTableWorldPosition) * tableReflectionMask;
|
float candleContact = candleContactField(vTableWorldPosition) * tableReflectionMask;
|
||||||
float candleProjectedShadow = candleProjectedShadowField(vTableWorldPosition) * tableReflectionMask;
|
float candleProjectedShadow = candleProjectedShadowField(vTableWorldPosition) * tableReflectionMask;
|
||||||
float candleOcclusion = clamp(candleContact + candleProjectedShadow * 0.42, 0.0, 0.48);
|
float candleOcclusion = clamp(candleContact + candleProjectedShadow * 0.42, 0.0, 0.48);
|
||||||
vec3 normalDebug = normalize(normal) * 0.5 + 0.5;
|
vec3 normalDebug = normalize(normal) * 0.5 + 0.5;
|
||||||
outgoingLight *= mix(1.0, contactAo, tableReflectionMask * 0.55);
|
outgoingLight *= mix(1.0, contactAo, tableReflectionMask * 0.55);
|
||||||
outgoingLight = mix(outgoingLight, reflectedSurface, tableReflectionMask * (0.07 + fresnel * 0.14) * reflectionCleanliness);
|
outgoingLight = mix(outgoingLight, reflectedSurface, tableReflectionMask * (0.16 + fresnel * 0.28 + greaseFilm * 0.065) * reflectionCleanliness);
|
||||||
outgoingLight += tableReflectionMask * roomReflection * 0.008 * reflectionCleanliness;
|
outgoingLight += tableReflectionMask * roomReflection * 0.004 * reflectionCleanliness;
|
||||||
outgoingLight = mix(outgoingLight, outgoingLight * vec3(0.72, 0.68, 0.6) + vec3(0.045, 0.036, 0.025), dustDulling * 0.32);
|
outgoingLight += tableReflectionMask * dustFilm * vec3(0.008, 0.0085, 0.009) * (0.22 + fresnel * 0.62);
|
||||||
|
outgoingLight += tableReflectionMask * greaseFilm * vec3(0.018, 0.013, 0.007) * (0.28 + fresnel * 0.58);
|
||||||
outgoingLight *= mix(vec3(1.0), vec3(0.5, 0.4, 0.31), candleOcclusion);
|
outgoingLight *= mix(vec3(1.0), vec3(0.5, 0.4, 0.31), candleOcclusion);
|
||||||
if (tableDebugMode == 1) outgoingLight = vec3(candleProjectedShadow);
|
if (tableDebugMode == 1) outgoingLight = vec3(candleProjectedShadow);
|
||||||
if (tableDebugMode == 2) outgoingLight = vec3(dust);
|
if (tableDebugMode == 2) outgoingLight = vec3(dust);
|
||||||
@@ -677,6 +720,7 @@ function configureTableShader(material) {
|
|||||||
if (tableDebugMode == 5) outgoingLight = sceneReflection;
|
if (tableDebugMode == 5) outgoingLight = sceneReflection;
|
||||||
if (tableDebugMode == 6) outgoingLight = vec3(tableReflectionMask);
|
if (tableDebugMode == 6) outgoingLight = vec3(tableReflectionMask);
|
||||||
if (tableDebugMode == 7) outgoingLight = vec3(candleContact);
|
if (tableDebugMode == 7) outgoingLight = vec3(candleContact);
|
||||||
|
if (tableDebugMode == 8) outgoingLight = vec3(grease);
|
||||||
#include <opaque_fragment>`
|
#include <opaque_fragment>`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -965,10 +1009,10 @@ function createTableNormalTexture() {
|
|||||||
function createTableDustTexture() {
|
function createTableDustTexture() {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
generatedTextureCanvases.tableDust = canvas;
|
generatedTextureCanvases.tableDust = canvas;
|
||||||
canvas.width = 2048;
|
canvas.width = 4096;
|
||||||
canvas.height = 2048;
|
canvas.height = 4096;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.fillStyle = 'rgb(10, 10, 10)';
|
ctx.fillStyle = 'rgb(1, 1, 1)';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
const image = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
const image = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
@@ -977,11 +1021,11 @@ function createTableDustTexture() {
|
|||||||
const i = (y * canvas.width + x) * 4;
|
const i = (y * canvas.width + x) * 4;
|
||||||
const nx = x / canvas.width;
|
const nx = x / canvas.width;
|
||||||
const ny = y / canvas.height;
|
const ny = y / canvas.height;
|
||||||
const edgeDust = Math.max(0, 1 - Math.min(nx, ny, 1 - nx, 1 - ny) * 9);
|
const edgeDust = Math.max(0, 1 - Math.min(nx, ny, 1 - nx, 1 - ny) * 18);
|
||||||
const grain = Math.random() < 0.018 ? 110 + Math.random() * 85 : Math.random() * 18;
|
const microNoise = Math.pow(Math.random(), 7.2) * 5.5;
|
||||||
const sweep = Math.max(0, Math.sin(nx * 32 + Math.sin(ny * 15) * 2.5) - 0.84) * 55;
|
const fineFilm = Math.max(0, Math.sin(nx * 21 + Math.sin(ny * 11) * 0.7) - 0.988) * 3;
|
||||||
const pageDust = Math.exp(-Math.pow((nx - 0.5) * 2.2, 2) - Math.pow((ny - 0.5) * 1.4, 2)) * 16;
|
const bookShelter = Math.exp(-Math.pow((nx - 0.5) * 2.7, 2) - Math.pow((ny - 0.5) * 1.8, 2)) * 1.5;
|
||||||
const value = Math.min(255, 10 + edgeDust * 36 + grain + sweep + pageDust);
|
const value = Math.min(255, 1 + edgeDust * 3 + microNoise + fineFilm + bookShelter);
|
||||||
image.data[i] = value;
|
image.data[i] = value;
|
||||||
image.data[i + 1] = value;
|
image.data[i + 1] = value;
|
||||||
image.data[i + 2] = value;
|
image.data[i + 2] = value;
|
||||||
@@ -990,6 +1034,36 @@ function createTableDustTexture() {
|
|||||||
}
|
}
|
||||||
ctx.putImageData(image, 0, 0);
|
ctx.putImageData(image, 0, 0);
|
||||||
|
|
||||||
|
ctx.globalCompositeOperation = 'screen';
|
||||||
|
ctx.fillStyle = 'rgba(230, 230, 230, 0.028)';
|
||||||
|
for (let i = 0; i < 1700; i += 1) {
|
||||||
|
const x = Math.random() * canvas.width;
|
||||||
|
const y = Math.random() * canvas.height;
|
||||||
|
const radius = 0.08 + Math.random() * 0.22;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
|
|
||||||
|
const texture = new THREE.CanvasTexture(canvas);
|
||||||
|
texture.colorSpace = THREE.NoColorSpace;
|
||||||
|
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||||
|
texture.magFilter = THREE.LinearFilter;
|
||||||
|
texture.generateMipmaps = true;
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTableGreaseTexture() {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
generatedTextureCanvases.tableGrease = canvas;
|
||||||
|
canvas.width = 4096;
|
||||||
|
canvas.height = 4096;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.fillStyle = 'rgb(0, 0, 0)';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
const smudge = (x, y, rx, ry, alpha) => {
|
const smudge = (x, y, rx, ry, alpha) => {
|
||||||
const gradient = ctx.createRadialGradient(x, y, 0, x, y, Math.max(rx, ry));
|
const gradient = ctx.createRadialGradient(x, y, 0, x, y, Math.max(rx, ry));
|
||||||
gradient.addColorStop(0, `rgba(220, 220, 220, ${alpha})`);
|
gradient.addColorStop(0, `rgba(220, 220, 220, ${alpha})`);
|
||||||
@@ -1005,10 +1079,44 @@ function createTableDustTexture() {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
smudge(canvas.width * 0.17, canvas.height * 0.38, 170, 70, 0.12);
|
smudge(canvas.width * 0.17, canvas.height * 0.38, 210, 88, 0.088);
|
||||||
smudge(canvas.width * 0.83, canvas.height * 0.24, 120, 58, 0.1);
|
smudge(canvas.width * 0.83, canvas.height * 0.24, 170, 76, 0.081);
|
||||||
smudge(canvas.width * 0.73, canvas.height * 0.76, 155, 64, 0.09);
|
smudge(canvas.width * 0.73, canvas.height * 0.76, 185, 78, 0.072);
|
||||||
smudge(canvas.width * 0.5, canvas.height * 0.52, 260, 110, 0.055);
|
smudge(canvas.width * 0.5, canvas.height * 0.52, 300, 120, 0.053);
|
||||||
|
|
||||||
|
const fingerprint = (x, y, rx, ry, rotation, alpha) => {
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(x, y);
|
||||||
|
ctx.rotate(rotation);
|
||||||
|
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, Math.max(rx, ry));
|
||||||
|
gradient.addColorStop(0, `rgba(235, 235, 235, ${alpha})`);
|
||||||
|
gradient.addColorStop(0.68, `rgba(200, 200, 200, ${alpha * 0.48})`);
|
||||||
|
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
|
||||||
|
ctx.clip();
|
||||||
|
ctx.strokeStyle = `rgba(245, 245, 245, ${alpha * 0.42})`;
|
||||||
|
ctx.lineWidth = 2.4;
|
||||||
|
for (let ridge = -0.7; ridge <= 0.7; ridge += 0.18) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(rx * ridge * 0.18, ry * ridge * 0.24, rx * (0.26 + Math.abs(ridge) * 0.58), ry * (0.2 + Math.abs(ridge) * 0.5), 0, Math.PI * 0.08, Math.PI * 1.92);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.globalCompositeOperation = 'screen';
|
||||||
|
for (let i = 0; i < 8; i += 1) {
|
||||||
|
const x = canvas.width * (0.2 + Math.random() * 0.62);
|
||||||
|
const y = canvas.height * (0.18 + Math.random() * 0.64);
|
||||||
|
fingerprint(x, y, 36 + Math.random() * 22, 14 + Math.random() * 8, Math.random() * Math.PI, 0.102 + Math.random() * 0.042);
|
||||||
|
}
|
||||||
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
|
|
||||||
const texture = new THREE.CanvasTexture(canvas);
|
const texture = new THREE.CanvasTexture(canvas);
|
||||||
texture.colorSpace = THREE.NoColorSpace;
|
texture.colorSpace = THREE.NoColorSpace;
|
||||||
|
|||||||
Reference in New Issue
Block a user