index.vert

#version 300 es

in vec2 aVertexTextureCoords;
in vec3 aVertexPosition;

uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;

out vec2 vTextureCoords;

void main() {
  vTextureCoords = aVertexTextureCoords;
  gl_Position = vec4(aVertexPosition, 1.0);
}

index.frag

#version 300 es

precision highp float;

const float PI = 3.1415926;

const float R_LUMINANCE = 0.298912;
const float G_LUMINANCE = 0.586611;
const float B_LUMINANCE = 0.114478;

// 正規化
float norm(float v, float minV, float maxV) {
  return (v - minV) / (maxV - minV);
}

// 範囲変換
float map(float v, float inMin, float inMax, float outMin, float outMax) {
  return mix(outMin, outMax, norm(v, inMin, inMax));
}

// fork from https://www.shadertoy.com/view/MsS3Wc
vec3 hsv2rgb(vec3 color) {
  // Hueを[0, 1]から[0, 6]へスケール
  float hue = color.x * 6.0;
  
  vec3 m = mod(hue + vec3(0.0, 4.0, 2.0), 6.0);
  vec3 a = abs(m - 3.0);
  vec3 rgb = clamp(a - 1.0, 0.0, 1.0);
    
  // 白とrgbを彩度(動径)に沿って補間したものに明度をかける
  return color.z * mix(vec3(1.0), rgb, color.y);
}

// グレースケール化
float toMonochrome(vec3 color) {
  return dot(color, vec3(R_LUMINANCE, G_LUMINANCE, B_LUMINANCE));
}

// 擬似カラー
// 暗い色を青に、明るい色を赤に
vec3 toPseudoColor(float gray) {
  float hue = map(gray, 0.0, 1.0, float(240.0 / 360.0), 0.0);
  vec3 hsv = vec3(hue, 1.0, 1.0);
  return hsv2rgb(hsv);
}

// ----------------------------------------------------------------------

uniform sampler2D uTexture0;
uniform bool uPseudoOn;

in vec2 vTextureCoords;

out vec4 fragColor;

void main() {
  vec2 texCoord = vec2(vTextureCoords.x, 1.0 - vTextureCoords.y);
  vec4 original = texture(uTexture0, texCoord);
  
  vec3 pseudo = uPseudoOn
    ? toPseudoColor(toMonochrome(original.rgb))
    : original.rgb;
  
  fragColor = vec4(pseudo, original.a);
}

render.ts

import { Space } from "@/lib/canvas/index"
import { Program } from "@/lib/webgl/program"
import { Scene } from "@/lib/webgl/scene"
import { Clock } from "@/lib/event/clock"
import { ControlUi } from "@/lib/gui/control-ui"
import { Texture } from "@/lib/webgl/texture"
import { UniformLoader } from "@/lib/webgl/uniform-loader"

import mainVertSrc from "./index.vert?raw"
import mainFragSrc from "./index.frag?raw"

import imageMountain from "@/assets/original/mountain_00003.jpg"
import imagePattern from "@/assets/original/usg-pattern.jpg"
import imageFireWorks from "@/assets/original/fireworks_00018.jpg"
import imageTree from "@/assets/original/tree-woods_00123.jpg"

export const onload = () => {
  const space = new Space("gl-canvas")
  const canvas = space.canvas
  const gl = space.gl
  if (!canvas || !gl) return

  let scene: Scene
  let program: Program
  let clock: Clock
  let textures: Texture[] = []

  const uniforms = new UniformLoader(gl, ["uPseudoOn"])

  const images = [
    { name: "木", image: imageTree },
    { name: "山", image: imageMountain },
    { name: "幾何パターン", image: imagePattern },
    { name: "花火", image: imageFireWorks }
  ]
  const imageNames = images.map((obj) => obj.name)
  let activeImage = 1

  const defaultPseudoOn = true

  const initGuiControls = () => {
    const ui = new ControlUi()
    ui.select("Image", images[activeImage].name, imageNames, (name) => {
      const idx = imageNames.indexOf(name)
      if (idx < 0) return
      activeImage = idx
      space.fitImage(textures[activeImage].image)
    })
    ui.boolean("Pseudo", defaultPseudoOn, (value) => {
      uniforms.boolean("uPseudoOn", value)
    })
  }

  const onResize = () => {
    space.fitImage(textures[activeImage].image)
    render()
  }

  const configure = async () => {
    gl.clearColor(1.0, 0.0, 0.0, 1.0)
    gl.clearDepth(1.0)

    program = new Program(gl, mainVertSrc, mainFragSrc)

    scene = new Scene(gl, program)
    clock = new Clock()

    await Promise.all(
      images.map(async (obj) => {
        const texture = new Texture(gl, program, obj.image)
        textures.push(texture)
        await texture.load()
      })
    )

    uniforms.init(program)
    uniforms.boolean("uPseudoOn", defaultPseudoOn)

    space.fitImage(textures[activeImage].image)
    space.onResize = onResize
  }

  const registerGeometry = () => {
    // 画面を覆う板ポリゴン
    const vertices = [-1.0, 1.0, 0.0, 1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0]
    const texCoords = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]
    const indices = [0, 2, 1, 2, 3, 1]
    scene.add({ vertices, indices, texCoords })
  }

  const render = () => {
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

    scene.traverseDraw((obj) => {
      obj.bind()

      textures[activeImage].use()
      gl.drawElements(gl.TRIANGLES, obj.indices.length, gl.UNSIGNED_SHORT, 0)

      obj.cleanup()
    })
  }

  const init = async () => {
    await configure()
    registerGeometry()
    clock.on("tick", render)

    initGuiControls()
  }

  init()
}