index.vert

#version 300 es

in vec3 aVertexPosition;
in vec3 aVertexNormal;
in vec4 aVertexColor;

uniform mat4 uModelMatrix;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;

out vec4 vColor;
out vec3 vNormal;
out vec3 vPosition; // モデル座標変換を行なったあとの頂点の位置

void main() {
  // 点光源から発された光のライトベクトルは、
  // モデル座標変換を行なったあとの頂点の位置を考慮したものでなければならない
  vPosition = (uModelMatrix * vec4(aVertexPosition, 1.0)).xyz;
  vNormal = aVertexNormal;
  vColor = aVertexColor;
  
  gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition, 1.0);
}

index.frag

#version 300 es

// 全ての浮動小数点型の変数に高い精度を指定
precision highp float;

in vec4 vColor;
in vec3 vNormal;
in vec3 vPosition;

uniform mat4 uInvModelMatrix;
uniform vec3 uLightPosition;
uniform vec3 uEyeDirection;
uniform vec4 uAmbientColor;

out vec4 fragColor;

// フォンシェーディングはピクセルごとの色の補間処理が必要になるため、
// ライティングの計算を、全てフラグメントシェーダに任せる
void main() {
  // 頂点の位置と点光源の位置を使ってライトベクトルをその都度算出しなければならない
  vec3 lightDirection = uLightPosition - vPosition;
  
  // モデルが回転などの座標変換を行なっていても、それと真逆の変換をライトベクトルに適用することで相殺する
  vec3 invLight = normalize(uInvModelMatrix * vec4(lightDirection, 0.0)).xyz;
  vec3 invEye = normalize(uInvModelMatrix * vec4(uEyeDirection, 0.0)).xyz;
  
  // ライトベクトルと視線ベクトルとのハーフベクトル
  vec3 halfLE = normalize(invLight + invEye);
  
  // ライト係数
  float diffuse = clamp(dot(vNormal, invLight), 0.0, 1.0) + 0.2;
  
  // 面法線ベクトルとの内積を取ることで反射光を計算
  // 反射光は強いハイライトを演出するためのものなので、
  // 内積によって得られた結果をべき乗によって収束させることで、
  // 弱い光をさらに弱く、強い光はそのまま残すという具合に変換させる
  float specular = pow(clamp(dot(vNormal, halfLE), 0.0, 1.0), 50.0);
  
  // 反射光は光の強さを直接表す係数として使うので、環境光と同じように加算処理で色成分に加える
  vec4 light = vColor * vec4(vec3(diffuse), 1.0) + vec4(vec3(specular), 1.0) + uAmbientColor;
  
  // 色 = 頂点色 * 拡散光 + 反射光 + 環境光
  fragColor = light;
}

render.ts

import { Space } from "@/lib/canvas/index"
import { Program } from "@/lib/webgl/program"
import vertexSource from "./index.vert?raw"
import fragmentSource from "./index.frag?raw"
import { Scene } from "@/lib/webgl/scene"
import { Camera } from "@/lib/webgl/camera"
import { Transforms } from "@/lib/webgl/transforms"
import { Matrix4 } from "@/lib/math/matrix"
import { torus } from "@/lib/shape/torus"
import { Vector3 } from "@/lib/math/vector"
import { Clock } from "@/lib/event/clock"
import { Light } from "@/lib/webgl/light"
import { sphere } from "@/lib/shape/sphere"

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 camera: Camera
  let transforms: Transforms
  let clock: Clock
  let light: Light
  let count = 0

  const rotateAxis = new Vector3(0.0, 1.0, 1.0).normalize()

  const onResize = () => {
    space.fitScreen()
    render()
  }

  const configure = () => {
    space.fitScreenSquare()

    // カリングと深度テストを有効にする
    gl.enable(gl.DEPTH_TEST)
    gl.depthFunc(gl.LEQUAL)
    gl.enable(gl.CULL_FACE)

    gl.clearColor(0.0, 0.0, 0.0, 1.0)
    gl.clearDepth(1.0)

    const program = new Program(gl, vertexSource, fragmentSource)

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

    camera = new Camera()
    camera.position = [0.0, 0.0, 20.0]
    camera.fov = 45
    camera.near = 0.1
    camera.far = 100
    camera.update()

    transforms = new Transforms(gl, program, camera, canvas)

    light = new Light(gl, program)
    light.position = [0.0, 0.0, 0.0]
    light.ambientColor = [0.1, 0.1, 0.1, 1.0]
    light.eye = [0.0, 0.0, 20.0]

    space.onResize = onResize
  }

  const regiisterGeometry = () => {
    const torusGeometry = torus(2.0, 1.0, 50, 50, [0.13, 0.826, 1.0, 1.0])
    const sphereGeometry = sphere(2.0, 64, 64, [1.0, 0.13, 0.957, 1.0])

    scene.add({ alias: "torus", ...torusGeometry })
    scene.add({ alias: "sphere", ...sphereGeometry })
  }

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

    count++
    const rad = ((count % 360) * Math.PI) / 180
    const tx = Math.cos(rad) * 3.5
    const ty = Math.sin(rad) * 3.5
    const tz = Math.sin(rad) * 3.5

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

      if (obj.alias === "torus") {
        const model = Matrix4.identity().translate(tx, -ty, -tz).rotateAround(rotateAxis, -rad)
        transforms.push(model)
        light.model = model
      }

      if (obj.alias === "sphere") {
        const model = Matrix4.identity().translate(-tx, ty, tz)
        transforms.push(model)
        light.model = model
      }

      transforms.pop()
      transforms.setMatrixUniforms()
      light.reflect()
      gl.drawElements(gl.TRIANGLES, obj.indices.length, gl.UNSIGNED_SHORT, 0)

      obj.cleanup()
    })
  }

  const init = () => {
    configure()
    regiisterGeometry()
    clock.on("tick", render)
  }

  init()
}