three.js dev template - module

HTML

<script type="importmap">
	{
		"imports": {
			"three": "https://unpkg.com/three/build/three.module.js",
      "three/addons/": "https://unpkg.com/three/examples/jsm/"
		}
	}
</script>

<script type="x-shader/x-fragment" id="fragment-shader">
  uniform sampler2D uTexture[TEXTURE_COUNT];

  varying vec2 vUv;
  varying vec4 vTextureUvBounds[TEXTURE_COUNT];
  varying vec2 vTextureUv[TEXTURE_COUNT];

  void main () {
      vec2 uv = vUv;
      vec4 color = vec4(0.0);
      float textureAlpha[TEXTURE_COUNT];

      bool outsideUv = uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0;

      for (int i = 0; i < TEXTURE_COUNT; i++) {
          vec2 textureUv = vTextureUv[i];
          bool outsideTextureUvBounds = textureUv.x < vTextureUvBounds[i].x || textureUv.x > vTextureUvBounds[i].y || textureUv.y < vTextureUvBounds[i].z || textureUv.y > vTextureUvBounds[i].w;

          if (outsideUv || outsideTextureUvBounds) {
              textureAlpha[i] = 0.0;
          } else {
              textureAlpha[i] = 1.0;
          }
      }

      #include <texture_sampling>

      gl_FragColor = color;
  }
</script>

<script type="x-shader/x-vertex" id="vertex-shader">
  uniform vec2 uTextureRes[TEXTURE_COUNT];
  uniform vec2 uRes;
  uniform float uTranslateX;
  
	varying vec2 vTextureUv[TEXTURE_COUNT];
	varying vec4 vTextureUvBounds[TEXTURE_COUNT];
  varying vec2 vUv;

	void main () {
  		vUv = uv;
  	 for (int i = 0; i < TEXTURE_COUNT; i++) {
        vTextureUv[i] = vUv;

        float textureAspect = uTextureRes[i].x / uTextureRes[i].y;
        float meshAspect = uRes.x / uRes.y;

        // fit: cover
        float scaleX = 1.0;

        if (textureAspect < meshAspect) {
            // Texture is "taller"; scale y
            float scale = textureAspect / meshAspect;
            vTextureUv[i].y = vTextureUv[i].y * scale - (scale - 1.0) * 0.5;

            // Calculate UV bounds for cropping
            vTextureUvBounds[i].xy = vec2(0.0, 1.0); // minX, maxX
           ...

CSS

body {
  margin: 0px;
}

JavaScript

import * as THREE from "three"
import { OrbitControls } from "three/addons/controls/OrbitControls.js"

init()

async function init() {
  const sources = [
    "https://picsum.photos/id/1/960/540",
  	"https://picsum.photos/id/10/540/960",
    "https://picsum.photos/id/20/960/540",
  ]
  const loader = new THREE.TextureLoader()
  const textures = await Promise.all(
    sources.map((src) => loader.loadAsync(src)),
  )

  // renderer
  const renderer = new THREE.WebGLRenderer()
  renderer.setSize(window.innerWidth, window.innerHeight)
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setAnimationLoop(animate)
  document.body.appendChild(renderer.domElement)

  // scene
  const scene = new THREE.Scene()

  // camera
  const camera = new THREE.PerspectiveCamera(
    40,
    window.innerWidth / window.innerHeight,
    1,
    10000,
  )
  camera.position.set(20, 20, 20)

  // controls
  const controls = new OrbitControls(camera, renderer.domElement)

  // axes
  scene.add(new THREE.AxesHelper(20))

  // geometry
  const geometry = new THREE.PlaneGeometry(10, 10)

  // material
  const material = new THREE.ShaderMaterial({
    defines: {
      TEXTURE_COUNT: textures.length,
    },
    uniforms: {
      uRes: {
        value: new THREE.Vector2(
          geometry.parameters.width,
          geometry.parameters.height,
        ),
      },
      uTranslateX: { value: 0 },
      uTextureRes: {
        value: textures.map(
          (t) => new THREE.Vector2(t.image.width, t.image.height),
        ),
      },
      uTexture: { value: textures }
    },
    side: THREE.DoubleSide,
    vertexShader: document.getElementById("vertex-shader").textContent,
    fragmentShader: document.getElementById("fragment-shader").textContent,
  })

  material.onBeforeCompile = (shader) => {
    let textureSampling = ""
    for (let i = 0; i < textures.length; i++) {
      textureSampling += `
        	vec4 textureColor${i} = texture2D(uTexture[${i}], vTextureUv[${i}]);
        	color...