body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 1;
const scene = new THREE.Scene();
// make our texture using a canvas to test
const bigImage = document.createElement('canvas');
{
const ctx = bigImage.getContext('2d');
const width = 32;
const height = 16;
ctx.canvas.width = width;
ctx.canvas.height = height;
const gradient = ctx.createLinearGradient(0, 0, width, height);
gradient.addColorStop(0, 'red');
gradient.addColorStop(0.5, 'yellow');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
const forceTextureInitialization = function() {
const material = new THREE.MeshBasicMaterial();
const geometry = new THREE.PlaneBufferGeometry();
const scene = new THREE.Scene();
scene.add(new THREE.Mesh(geometry, material));
const camera = new THREE.Camera();
return function forceTextureInitialization(texture) {
material.map = texture;
renderer.render(scene, camera);
};
}();
// bad
{
const ctx = document.createElement('canvas').getContext('2d');
// split the texture into 4 parts across 4 planes
const across = 2;
const down = 2;
const pixelsAcross = bigImage.width / across;
const pixelsDown = bigImage.height / down;
ctx.canvas.width = pixelsAcross;
ctx.canvas.height = pixelsDown;
for (let y = 0; y < down; ++y) {
for (let x = 0; x < across; ++x) {
ctx.clearRect(0, 0, pixelsAcross, pixelsDown);
ctx.drawImage(bigImage,
x * pixelsAcross, (down - 1 - y) * pixelsDown, pixelsAcross, pixelsDown,
0, 0, pixelsAcross, pixelsDown);
const texture = new THREE.CanvasTexture(ctx.canvas);
// see https://threejsfundamentals.org/threejs/lessons/threejs-canvas-textures.html
forceTextureInitialization(texture);
const geometry = new THREE.PlaneBufferGeometry(1 / across, 1 / down);
const material = new THREE.MeshBasicMaterial({map: texture});
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
plane.position.set(-1 + x / across, y / down - 0.25, 0);
}
}
}
// good
{
const ctx = document.createElement('canvas').getContext('2d');
// split the texture into 4 parts across 4 planes
const across = 2;
const down = 2;
const pixelsAcross = bigImage.width / across;
const pixelsDown = bigImage.height / down;
ctx.canvas.width = pixelsAcross + 2;
ctx.canvas.height = pixelsDown + 2;
// just draw the image at all these offsets.
// it would be more efficient to draw the edges
// 1 pixel wide but I'm lazy
const offsets = [
[ 0, 0],
[ 1, 0],
[ 2, 0],
[ 0, 1],
[ 2, 1],
[ 0, 2],
[ 1, 2],
[ 2, 2],
[ 1, 1],
];
for (let y = 0; y < down; ++y) {
for (let x = 0; x < across; ++x) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
let srcX = x * pixelsAcross - 1;
let srcY = (down - 1 - y) * pixelsDown - 1;
let dstX = 0;
let dstY = 0;
let width = pixelsAcross + 2;
let height = pixelsDown + 2;
ctx.drawImage(bigImage,
srcX, srcY, width, height,
dstX, dstY, width, height);
// handle edges
if (srcX < 0) {
// repeat left edge
ctx.drawImage(bigImage,
0, srcY, 1, height,
0, dstY, 1, height);
}
if (srcY < 0) {
// repeat top edge
ctx.drawImage(bigImage,
srcX, 0, width, 1,
dstX, 0, width, 1);
}
if (srcX + width > bigImage.width) {
// repeat right edge
ctx.drawImage(bigImage,
bigImage.width - 1, srcY, 1, height,
ctx.canvas.width - 1, dstY, 1, height);
}
if (srcY + height > bigImage.height) {
// repeat bottom edge
ctx.drawImage(bigImage,
srcX, bigImage.height - 1, width, 1,
dstX, ctx.canvas.height - 1, width, 1);
}
// TODO: handle corners
const texture = new THREE.CanvasTexture(ctx.canvas);
texture.minFilter = THREE.LinearFilter;
// offset UV coords 1 pixel to skip the edge pixel
texture.offset.set(1 / ctx.canvas.width, 1 / ctx.canvas.height);
// only textureSize - 2 of the pixels in the texture
texture.repeat.set(pixelsAcross / ctx.canvas.width, pixelsDown / ctx.canvas.height);
// see https://threejsfundamentals.org/threejs/lessons/threejs-canvas-textures.html
forceTextureInitialization(texture);
const geometry = new THREE.PlaneBufferGeometry(1 / across, 1 / down);
const material = new THREE.MeshBasicMaterial({map: texture});
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
plane.position.set(1 + x / across - 0.5, y / down - 0.25, 0);
}
}
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
</script>