///
import { arrayFromFunction, clamp, DEG, Tuple2, V, V3 } from "ts3dutils"
import {
isWebGL2RenderingContext,
Mesh,
Shader,
Texture,
TSGLContext,
} from "tsgl"
import rayTracerFS from "../shaders/rayTracerFS.glslx"
import rayTracerVS from "../shaders/rayTracerVS.glslx"
/**
* Realtime GPU ray tracing including reflection.
*/
export async function rayTracing(gl: TSGLContext) {
if (!isWebGL2RenderingContext(gl)) throw new Error("require webgl2")
let angleX = 30
let angleY = 10
// This is the mesh we tell WebGL to draw. It covers the whole view so each pixel will get a fragment shader call.
const mesh = Mesh.plane({ startX: -1, startY: -1, width: 2, height: 2 })
// floor and dodecahedron are meshes we will ray-trace
// add a vertex buffer "specular", which defines how reflective the mesh is.
// specular=1 means it is perfectly reflective, specular=0 perfectly matte
// meshes neeed coords vertex buffer as we will draw them with meshes
const floor = Mesh.plane({ startX: -4, startY: -4, width: 8, height: 8 })
.addVertexBuffer("specular", "specular")
.rotateX(90 * DEG)
floor.specular = floor.vertices.map((_) => 0) // floor doesn't reflect
const dodecahedron = Mesh.sphere(0)
.addVertexBuffer("specular", "specular")
.addVertexBuffer("coords", "ts_TexCoord")
.translate(3, 1)
// d20 reflects most of the light
dodecahedron.specular = dodecahedron.vertices.map((_) => 0.8)
// all uv coordinates the same to pick a solid color from the texture
dodecahedron.coords = dodecahedron.vertices.map(
(_) => [0, 0] as Tuple2,
)
// don't transform the vertices at all
// out/in pos so we get the world position of the fragments
const shader = Shader.create(rayTracerVS, rayTracerFS)
// define spheres which we will have the shader ray-trace
const sphereCenters = arrayFromFunction(
8,
(i) => [V(0.0, 1.6, 0.0), V(3, 3, 3), V(-3, 3, 3)][i] || V3.O,
)
const sphereRadii = arrayFromFunction(8, (i) => [1.5, 0.5, 0.5][i] || 0)
// texture for ray-traced mesh
const floorTexture = await Texture.fromURL("./mandelbrot.jpg")
const showMesh = floor.concat(dodecahedron)
const textureWidth = 1024
const textureHeight = 1
// verticesTexture contains the mesh vertices
// vertices are unpacked so we don't have an extra index buffer for the triangles
const verticesTexture = new Texture(textureWidth, textureHeight)
const verticesBuffer = new Float32Array(textureWidth * textureHeight * 3)
V3.pack(
showMesh.TRIANGLES.map((i) => showMesh.vertices[i]),
verticesBuffer,
)
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGB32F,
textureWidth,
textureHeight,
0,
gl.RGB,
gl.FLOAT,
verticesBuffer,
)
// uvTexture contains the uv coordinates for the vertices as wel as the specular value for each vertex
const uvTexture = new Texture(textureWidth, textureHeight, {
format: gl.RGB,
type: gl.FLOAT,
})
const uvBuffer = new Float32Array(textureWidth * textureHeight * 3)
showMesh.TRIANGLES.forEach((i, index) => {
uvBuffer[index * 3] = showMesh.coords[i][0]
uvBuffer[index * 3 + 1] = showMesh.coords[i][1]
uvBuffer[index * 3 + 2] = showMesh.specular[i]
})
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGB32F,
textureWidth,
textureHeight,
0,
gl.RGB,
gl.FLOAT,
uvBuffer,
)
let lastPos = V3.O
// scene rotation
gl.canvas.onmousemove = function (e) {
const pagePos = V(e.pageX, e.pageY)
const delta = lastPos.to(pagePos)
if (e.buttons & 1) {
angleY += delta.x
angleX = clamp(angleX + delta.y, -90, 90)
}
lastPos = pagePos
}
gl.matrixMode(gl.PROJECTION)
gl.loadIdentity()
verticesTexture.bind(0)
floorTexture.bind(1)
uvTexture.bind(2)
shader.uniforms({
"sphereCenters[0]": sphereCenters,
"sphereRadii[0]": sphereRadii,
vertices: 0,
triangleTexture: 1,
texCoords: 2,
})
return gl.animate(function (_abs, _diff) {
// Camera setup
gl.matrixMode(gl.MODELVIEW)
gl.loadIdentity()
// gl.perspective(70, gl.canvas.width / gl.canvas.height, 0.1, 1000)
// gl.lookAt(V(0, 200, 200), V(0, 0, 0), V3.Z)
gl.translate(0, 0, -10)
gl.rotate(angleX, 1, 0, 0)
gl.rotate(angleY, 0, 1, 0)
gl.scale(0.2)
shader.draw(mesh)
// Draw debug output to show that the raytraced scene lines up correctly with
// the rasterized scene
gl.color(0, 0, 0, 0.5)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.begin(gl.LINES)
for (let s = 4, i = -s; i <= s; i++) {
gl.vertex(-s, 0, i)
gl.vertex(s, 0, i)
gl.vertex(i, 0, -s)
gl.vertex(i, 0, s)
}
gl.end()
gl.disable(gl.BLEND)
})
}
;(rayTracing as any).info = "LMB-drag to rotate camera."