Textures and CORS

Loading Textures and Handling CORS

TextureLoader fetches an image through an <img> element or createImageBitmap and returns a Texture at once, filling it when the image arrives; loadAsync() waits instead. loadCovers() in bookshelf.js loads WebGL's six cover images this way, and createShelf(covers) gives each book six face materials, the cover on +z:

bookshelf.js: loadCovers(), six cover images as sRGB texturesJavaScript
export async function loadCovers(manager) {
  const loader = new THREE.TextureLoader(manager);
  return Promise.all(BOOKS.map(async (book, i) => {
    const texture = await loader.loadAsync(`booknest-cover-${i + 1}.png`);
    texture.colorSpace = THREE.SRGBColorSpace;      // a color map (Section 5.4.5)
    texture.anisotropy = 8;
    return texture;
  }));
}
BookNest's shelf with its cover artHTML
<script src="three-map.js"></script>
<script type="module">
import { createStage, createShelf, loadCovers, renderFrames } from './bookshelf.js';
const { renderer, scene, camera } = createStage({ width: 640, height: 190 });
scene.add(createShelf(await loadCovers()));             // top-level await in a module
renderFrames(renderer, scene, camera);
</script>
Browser output of Listing 5.18
Browser output of 18

Images from another origin need that server's Access-Control-Allow-Origin header: loaders request them with crossOrigin = 'anonymous', and WebGL refuses to upload an image that fails the CORS check (CORS and Untrusted Assets covers tainted canvases). Pages opened as file:// have no origin at all, so serve them over HTTP.

TextureLoader.loadAsync filling six book covers, each set to SRGBColorSpaceHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
  canvas { display: block; max-width: 100%; }
  p { margin: 4px 8px; }
</style>
<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<p id="out">Loading covers…</p>
<script type="module">
import * as THREE from 'three';

const BOOKS = [
  { title: 'The Quiet Harbor', author: 'Elena Marsh', color: '#1f5f8b' },
  { title: 'Patterns of the Deep Web', author: 'Ravi Menon', color: '#5b3f99' },
  { title: 'Salt and Saffron', author: 'Leila Haddad', color: '#e09a10' },
  { title: 'Small Steps to Big Summits', author: 'Tom Okafor', color: '#3f7d3a' },
  { title: "The Clockmaker's Paradox", author: 'Iris Vale', color: '#b5452f' },
  { title: 'Gardens in Glass', author: 'June Park', color: '#2a9d8f' },
];

// Stand-ins for booknest-cover-N.png: PNG data URLs, so the demo needs no server.
// Real covers from another origin need Access-Control-Allow-Origin, or WebGL refuses them.
function coverURL(book) {
  const c = Object.assign(document.createElement('canvas'), { width: 256, height: 360 });
  const g = c.getContext('2d');
  g.fillStyle = book.color;  g.fillRect(0, 0, 256, 360);
  g.fillStyle = 'rgb(0 0 0 / 0.2)';  g.fillRect(0, 0, 18, 360);
  g.fillStyle = '#fff3e0';  g.font = 'bold 34px Georgia';
  book.title.split(' ').forEach((w, i) => g.fillText(w, 30, 60 + i * 40, 210));
  g.font = 'italic 20px Georgia';  g.fillText(book.author, 30, 330);
  return c.toDataURL('image/png');
}

async function loadCovers() {
  const loader = new THREE.TextureLoader();
  return Promise.all(BOOKS.map(async (book) => {
    const texture = await loader.loadAsync(coverURL(book));
    texture.colorSpace = THREE.SRGBColorSpace;         // a color map
    texture.anisotropy = 8;
    return texture;
  }));
}

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 240);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 600 / 240, 0.1, 50);
camera.position.set(0, 0.2, 2.5);
camera.lookAt(0, -0.05, 0);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.5));
const sun = new THREE.DirectionalLight('#ffffff', 2);
sun.position.set(2, 3, 4);
scene.add(sun);
const plank = new THREE.Mesh(new THREE.BoxGeometry(3.2, 0.06, 0.4), new THREE.MeshStandardMaterial({ color: '#8d6e63' }));
plank.position.y = -0.52;
scene.add(plank);

const covers = await loadCovers();                     // top-level await in a module
const pages = new THREE.MeshStandardMaterial({ color: '#f3ecdc' });
const books = BOOKS.map((book, i) => {
  const board = new THREE.MeshStandardMaterial({ color: book.color });
  // BoxGeometry face order: +x, -x, +y, -y, +z (the cover), -z
  const mesh = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.6, 0.08),
    [pages, board, pages, pages, new THREE.MeshStandardMaterial({ map: covers[i] }), board]);
  mesh.position.set((i - 2.5) * 0.5, -0.19, 0);
  scene.add(mesh);
  return mesh;
});
document.getElementById('out').textContent =
  `${covers.length} textures loaded, ${covers[0].image.width}×${covers[0].image.height}, colorSpace ${covers[0].colorSpace}`;

renderer.setAnimationLoop((time) => {
  books.forEach((b, i) => { b.rotation.y = Math.sin(time / 1000 + i) * 0.35; });
  renderer.render(scene, camera);
});
</script>