index.vert

#version 300 es

in vec3 aVertexPosition;
in vec3 aVertexNormal;
in vec4 aVertexColor;
in vec2 aVertexTextureCoords;

uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat4 uNormalMatrix;
uniform vec3 uLightPosition;

out vec2 vTextureCoords;
out vec3 vTangentLightDirection;
out vec3 vTangentEyeDirection;

void main() {
  vec4 vertex = uModelViewMatrix * vec4(aVertexPosition, 1.0);
  
  vec3 normal = vec3(uNormalMatrix * vec4(aVertexNormal, 1.0));
  vec3 tangent = cross(normal, vec3(0.0, 1.0, 0.0));
  vec3 bitangent = cross(normal, tangent);
  
  mat3 toTangentSpace = mat3(
    tangent.x, bitangent.x, normal.x,
    tangent.y, bitangent.y, normal.y,
    tangent.z, bitangent.z, normal.z
  );
  
  vec3 eyeDirection = -vertex.xyz;
  vec3 lightDirection = uLightPosition - vertex.xyz;
  
  vTangentEyeDirection = eyeDirection * toTangentSpace;
  vTangentLightDirection = lightDirection * toTangentSpace;
  vTextureCoords = aVertexTextureCoords;
  gl_Position = uProjectionMatrix * vertex;
}

index.frag

#version 300 es

precision highp float;

in vec2 vTextureCoords;
in vec3 vTangentLightDirection;
in vec3 vTangentEyeDirection;

uniform sampler2D uTexture0; // texture
uniform sampler2D uTexture1; // normal map
uniform vec4 uMaterialDiffuse;
uniform vec4 uMaterialAmbient;
uniform vec4 uLightAmbient;
uniform vec4 uLightDiffuse;

out vec4 fragColor;

void main() {
  vec3 N = normalize(2.0 * vec3(texture(uTexture1, vTextureCoords)) - 0.5);
  vec3 L = normalize(vTangentLightDirection);
  
  float lambertTerm = max(dot(N, L), 0.2);
  
  vec3 E = normalize(vTangentEyeDirection);
  vec3 R = reflect(-L, N);
  
  float Is = pow(clamp(dot(R, E), 0.0, 1.0), 8.0);
  
  vec4 Ia = uLightAmbient * uMaterialAmbient;
  vec4 Id = uLightDiffuse * uMaterialDiffuse * texture(uTexture0, vTextureCoords) * lambertTerm;
  
  fragColor = Ia + Id + Is;
}

render.ts

import { Space } from "@/lib/canvas/index"
import { Program } from "@/lib/webgl/program"
import { Scene } from "@/lib/webgl/scene"
import { Transforms } from "@/lib/webgl/transforms"
import { Clock } from "@/lib/event/clock"
import { AngleCamera } from "@/lib/camera/angle-camera"
import { AngleCameraController } from "@/lib/control/angle-camera-controller"
import { Texture } from "@/lib/webgl/texture"
import { Light } from "@/lib/light/light"

import vertexSource from "./index.vert?raw"
import fragmentSource from "./index.frag?raw"

import cubeModel from "@/lib/model/cube-texture.json" assert { type: "json" }

import image from "@/assets/542x542/usg-pattern.png"
import normalMap from "@/assets/normal-map/usg-pattern.png"

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 camera: AngleCamera
  let transforms: Transforms
  let clock: Clock
  let light: Light
  let texture: Texture
  let normalTexture: Texture

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

  const configure = async () => {
    space.fitHorizontal()

    gl.clearColor(0.9, 0.9, 0.9, 1)
    gl.clearDepth(100)

    gl.enable(gl.DEPTH_TEST)
    gl.depthFunc(gl.LESS)

    program = new Program(gl, vertexSource, fragmentSource)
    scene = new Scene(gl, program)

    light = new Light(gl, program)
    light.position = [0, 5, 20]
    light.diffuse = [1, 1, 1, 1]
    light.ambient = [1, 1, 1, 1]
    light.useMaterial = ["ambient", "diffuse"]
    light.setUniformLocations()
    light.setUniforms()

    camera = new AngleCamera("ORBIT")
    camera.goHome([0, 0, 2.5])
    camera.azimuth = 40
    camera.elevation = -30
    camera.focus = [0, 0, 0]
    camera.update()

    new AngleCameraController(canvas, camera)

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

    texture = new Texture(gl, program, image, 0)
    await texture.load()

    normalTexture = new Texture(gl, program, normalMap, 1)
    await normalTexture.load()

    space.onResize = onResize
  }

  const registerGeometry = () => {
    scene.add({ alias: "cube", ...cubeModel, ambient: [0.2, 0.2, 0.2, 1] })
  }

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

    transforms.ModelView = camera.View
    transforms.setMatrixUniforms()

    texture.use()
    normalTexture.use()

    scene.traverseDraw((obj) => {
      obj.material?.setUniforms()
      obj.bind()

      gl.drawElements(gl.TRIANGLES, obj.indices.length, gl.UNSIGNED_SHORT, 0)

      obj.cleanup()
    })
  }

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

  init()
}