diff --git a/public/img/brickwall_diffuse.png b/public/img/brickwall_diffuse.png new file mode 100644 index 00000000..35835088 Binary files /dev/null and b/public/img/brickwall_diffuse.png differ diff --git a/public/img/brickwall_height.png b/public/img/brickwall_height.png new file mode 100644 index 00000000..48ab26fa Binary files /dev/null and b/public/img/brickwall_height.png differ diff --git a/public/img/brickwall_normal.png b/public/img/brickwall_normal.png new file mode 100644 index 00000000..aa6643de Binary files /dev/null and b/public/img/brickwall_normal.png differ diff --git a/public/img/spiral_height.png b/public/img/spiral_height.png new file mode 100644 index 00000000..1f1680ff Binary files /dev/null and b/public/img/spiral_height.png differ diff --git a/public/img/spiral_normal.png b/public/img/spiral_normal.png new file mode 100644 index 00000000..5cba15cf Binary files /dev/null and b/public/img/spiral_normal.png differ diff --git a/public/img/toybox_height.png b/public/img/toybox_height.png new file mode 100644 index 00000000..9977210b Binary files /dev/null and b/public/img/toybox_height.png differ diff --git a/public/img/toybox_normal.png b/public/img/toybox_normal.png new file mode 100644 index 00000000..91bcb56a Binary files /dev/null and b/public/img/toybox_normal.png differ diff --git a/public/img/wood_diffuse.png b/public/img/wood_diffuse.png new file mode 100644 index 00000000..e28e2aee Binary files /dev/null and b/public/img/wood_diffuse.png differ diff --git a/src/components/SampleLayout.module.css b/src/components/SampleLayout.module.css index 539be978..d7d530fc 100644 --- a/src/components/SampleLayout.module.css +++ b/src/components/SampleLayout.module.css @@ -21,6 +21,7 @@ nav.sourceFileNav li { display: inline-block; margin: 0; padding: 0; + transition: 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); } nav.sourceFileNav li a { diff --git a/src/meshes/box.ts b/src/meshes/box.ts new file mode 100644 index 00000000..b15ea483 --- /dev/null +++ b/src/meshes/box.ts @@ -0,0 +1,347 @@ +import { vec3 } from 'wgpu-matrix'; +import { getMeshPosAtIndex, getMeshUVAtIndex, Mesh } from './mesh'; + +export interface BoxMesh extends Mesh { + vertices: Float32Array; + indices: Uint16Array | Uint32Array; + vertexStride: number; +} + +//// Borrowed and simplified from https://github.com/mrdoob/three.js/blob/master/src/geometries/BoxGeometry.js +//// Presumes vertex buffer alignment of verts, normals, and uvs +const createBoxGeometry = ( + width = 1.0, + height = 1.0, + depth = 1.0, + widthSegments = 1.0, + heightSegments = 1.0, + depthSegments = 1.0 +) => { + widthSegments = Math.floor(widthSegments); + heightSegments = Math.floor(heightSegments); + depthSegments = Math.floor(depthSegments); + + const indices = []; + const vertNormalUVBuffer = []; + + let numVertices = 0; + + const buildPlane = ( + u: 0 | 1 | 2, + v: 0 | 1 | 2, + w: 0 | 1 | 2, + udir: -1 | 1, + vdir: -1 | 1, + planeWidth: number, + planeHeight: number, + planeDepth: number, + xSections: number, + ySections: number + ) => { + const segmentWidth = planeWidth / xSections; + const segmentHeight = planeHeight / ySections; + + const widthHalf = planeWidth / 2; + const heightHalf = planeHeight / 2; + const depthHalf = planeDepth / 2; + + const gridX1 = xSections + 1; + const gridY1 = ySections + 1; + + let vertexCounter = 0; + + const vertex = vec3.create(); + const normal = vec3.create(); + for (let iy = 0; iy < gridY1; iy++) { + const y = iy * segmentHeight - heightHalf; + + for (let ix = 0; ix < gridX1; ix++) { + const x = ix * segmentWidth - widthHalf; + + //Calculate plane vertices + vertex[u] = x * udir; + vertex[v] = y * vdir; + vertex[w] = depthHalf; + vertNormalUVBuffer.push(...vertex); + + //Caclulate normal + normal[u] = 0; + normal[v] = 0; + normal[w] = planeDepth > 0 ? 1.0 : -1.0; + vertNormalUVBuffer.push(...normal); + + //Calculate uvs + vertNormalUVBuffer.push(ix / xSections); + vertNormalUVBuffer.push(1 - iy / ySections); + + vertexCounter += 1; + } + } + + for (let iy = 0; iy < ySections; iy++) { + for (let ix = 0; ix < xSections; ix++) { + const a = numVertices + ix + gridX1 * iy; + const b = numVertices + ix + gridX1 * (iy + 1); + const c = numVertices + (ix + 1) + gridX1 * (iy + 1); + const d = numVertices + (ix + 1) + gridX1 * iy; + + //Push vertex indices + //6 indices for each face + indices.push(a, b, d); + indices.push(b, c, d); + + numVertices += vertexCounter; + } + } + }; + + //Side face + buildPlane( + 2, //z + 1, //y + 0, //x + -1, + -1, + depth, + height, + width, + depthSegments, + heightSegments + ); + + //Side face + buildPlane( + 2, //z + 1, //y + 0, //x + 1, + -1, + depth, + height, + -width, + depthSegments, + heightSegments + ); + + //Bottom face + buildPlane( + 0, //x + 2, //z + 1, //y + 1, + 1, + width, + depth, + height, + widthSegments, + depthSegments + ); + + //Top face + buildPlane( + 0, //x + 2, //z + 1, //y + 1, + -1, + width, + depth, + -height, + widthSegments, + depthSegments + ); + + //Side faces + buildPlane( + 0, //x + 1, //y + 2, //z + 1, + -1, + width, + height, + depth, + widthSegments, + heightSegments + ); + + //Side face + buildPlane( + 0, //x + 1, //y + 2, //z + -1, + -1, + width, + height, + -depth, + widthSegments, + heightSegments + ); + + return { + vertices: vertNormalUVBuffer, + indices: indices, + }; +}; + +type IndexFormat = 'uint16' | 'uint32'; + +// Box mesh code ported from threejs, with addition of indexFormat specifier for vertex pulling +export const createBoxMesh = ( + width = 1.0, + height = 1.0, + depth = 1.0, + widthSegments = 1.0, + heightSegments = 1.0, + depthSegments = 1.0, + indexFormat: IndexFormat = 'uint16' +): Mesh => { + const { vertices, indices } = createBoxGeometry( + width, + height, + depth, + widthSegments, + heightSegments, + depthSegments + ); + + const vertexStride = 8 * Float32Array.BYTES_PER_ELEMENT; //calculateVertexStride(vertexProperties); + + const indicesArray = + indexFormat === 'uint16' + ? new Uint16Array(indices) + : new Uint32Array(indices); + + return { + vertices: new Float32Array(vertices), + indices: indicesArray, + vertexStride: vertexStride, + }; +}; + +export const createBoxMeshWithTangents = ( + width = 1.0, + height = 1.0, + depth = 1.0, + widthSegments = 1.0, + heightSegments = 1.0, + depthSegments = 1.0 +): Mesh => { + const mesh = createBoxMesh( + width, + height, + depth, + widthSegments, + heightSegments, + depthSegments + ); + + const originalStrideElements = + mesh.vertexStride / Float32Array.BYTES_PER_ELEMENT; + + const vertexCount = mesh.vertices.length / originalStrideElements; + + const tangents = new Array(vertexCount); + const bitangents = new Array(vertexCount); + const counts = new Array(vertexCount); + for (let i = 0; i < vertexCount; i++) { + tangents[i] = [0, 0, 0]; + bitangents[i] = [0, 0, 0]; + counts[i] = 0; + } + + for (let i = 0; i < mesh.indices.length; i += 3) { + const [idx1, idx2, idx3] = [ + mesh.indices[i], + mesh.indices[i + 1], + mesh.indices[i + 2], + ]; + + const [pos1, pos2, pos3] = [ + getMeshPosAtIndex(mesh, idx1), + getMeshPosAtIndex(mesh, idx2), + getMeshPosAtIndex(mesh, idx3), + ]; + + const [uv1, uv2, uv3] = [ + getMeshUVAtIndex(mesh, idx1), + getMeshUVAtIndex(mesh, idx2), + getMeshUVAtIndex(mesh, idx3), + ]; + + const edge1 = vec3.sub(pos2, pos1); + const edge2 = vec3.sub(pos3, pos1); + const deltaUV1 = vec3.sub(uv2, uv1); + const deltaUV2 = vec3.sub(uv3, uv1); + + // Edge of a triangle moves in both u and v direction (2d) + // deltaU * tangent vector + deltav * bitangent + // Manipulating the data into matrices, we get an equation + + const constantVal = + 1.0 / (deltaUV1[0] * deltaUV2[1] - deltaUV1[1] * deltaUV2[0]); + + const tangent = [ + constantVal * (deltaUV2[1] * edge1[0] - deltaUV1[1] * edge2[0]), + constantVal * (deltaUV2[1] * edge1[1] - deltaUV1[1] * edge2[1]), + constantVal * (deltaUV2[1] * edge1[2] - deltaUV1[1] * edge2[2]), + ]; + + const bitangent = [ + constantVal * (-deltaUV2[0] * edge1[0] + deltaUV1[0] * edge2[0]), + constantVal * (-deltaUV2[0] * edge1[1] + deltaUV1[0] * edge2[1]), + constantVal * (-deltaUV2[0] * edge1[2] + deltaUV1[0] * edge2[2]), + ]; + + //Accumulate tangents and bitangents + tangents[idx1] = vec3.add(tangents[idx1], tangent); + bitangents[idx1] = vec3.add(bitangents[idx1], bitangent); + tangents[idx2] = vec3.add(tangents[idx2], tangent); + bitangents[idx2] = vec3.add(bitangents[idx2], bitangent); + tangents[idx3] = vec3.add(tangents[idx3], tangent); + bitangents[idx3] = vec3.add(bitangents[idx3], bitangent); + + //Increment index count + counts[idx1]++; + counts[idx2]++; + counts[idx3]++; + } + + for (let i = 0; i < tangents.length; i++) { + tangents[i] = vec3.divScalar(tangents[i], counts[i]); + bitangents[i] = vec3.divScalar(bitangents[i], counts[i]); + } + + const newStrideElements = 14; + const wTangentArray = new Float32Array(vertexCount * newStrideElements); + + for (let i = 0; i < vertexCount; i++) { + //Copy original vertex data (pos, normal uv) + wTangentArray.set( + //Get the original vertex [8 elements] (3 ele pos, 3 ele normal, 2 ele uv) + mesh.vertices.subarray( + i * originalStrideElements, + (i + 1) * originalStrideElements + ), + //And put it at the proper location in the new array [14 bytes = 8 og + 6 empty] + i * newStrideElements + ); + //For each vertex, place tangent after originalStride + wTangentArray.set( + tangents[i], + i * newStrideElements + originalStrideElements + ); + //Place bitangent after 3 elements of tangent + wTangentArray.set( + bitangents[i], + i * newStrideElements + originalStrideElements + 3 + ); + } + + return { + vertices: wTangentArray, + indices: mesh.indices, + vertexStride: mesh.vertexStride + Float32Array.BYTES_PER_ELEMENT * 3 * 2, + }; +}; diff --git a/src/meshes/mesh.ts b/src/meshes/mesh.ts new file mode 100644 index 00000000..0f4a42c7 --- /dev/null +++ b/src/meshes/mesh.ts @@ -0,0 +1,96 @@ +import { vec3, vec2 } from 'wgpu-matrix'; + +// Defines what to pass to pipeline to render mesh +export interface Renderable { + vertexBuffer: GPUBuffer; + indexBuffer: GPUBuffer; + indexCount: number; + bindGroup?: GPUBindGroup; +} + +export interface Mesh { + vertices: Float32Array; + indices: Uint16Array | Uint32Array; + vertexStride: number; +} + +/** + * @param {GPUDevice} device - A valid GPUDevice. + * @param {Mesh} mesh - An indexed triangle-list mesh, containing its vertices, indices, and vertexStride (number of elements per vertex). + * @param {boolean} storeVertices - A boolean flag indicating whether the vertexBuffer should be available to use as a storage buffer. + * @returns {boolean} An object containing an array of bindGroups and the bindGroupLayout they implement. + */ +export const createMeshRenderable = ( + device: GPUDevice, + mesh: Mesh, + storeVertices = false, + storeIndices = false +): Renderable => { + // Define buffer usage + const vertexBufferUsage = storeVertices + ? GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE + : GPUBufferUsage.VERTEX; + const indexBufferUsage = storeIndices + ? GPUBufferUsage.INDEX | GPUBufferUsage.STORAGE + : GPUBufferUsage.INDEX; + + // Create vertex and index buffers + const vertexBuffer = device.createBuffer({ + size: mesh.vertices.byteLength, + usage: vertexBufferUsage, + mappedAtCreation: true, + }); + new Float32Array(vertexBuffer.getMappedRange()).set(mesh.vertices); + vertexBuffer.unmap(); + + const indexBuffer = device.createBuffer({ + size: mesh.indices.byteLength, + usage: indexBufferUsage, + mappedAtCreation: true, + }); + + // Determine whether index buffer is indices are in uint16 or uint32 format + if ( + mesh.indices.byteLength === + mesh.indices.length * Uint16Array.BYTES_PER_ELEMENT + ) { + new Uint16Array(indexBuffer.getMappedRange()).set(mesh.indices); + } else { + new Uint32Array(indexBuffer.getMappedRange()).set(mesh.indices); + } + + indexBuffer.unmap(); + + return { + vertexBuffer, + indexBuffer, + indexCount: mesh.indices.length, + }; +}; + +export const getMeshPosAtIndex = (mesh: Mesh, index: number) => { + const arr = new Float32Array( + mesh.vertices.buffer, + index * mesh.vertexStride + 0, + 3 + ); + return vec3.fromValues(arr[0], arr[1], arr[2]); +}; + +export const getMeshNormalAtIndex = (mesh: Mesh, index: number) => { + const arr = new Float32Array( + mesh.vertices.buffer, + index * mesh.vertexStride + 3 * Float32Array.BYTES_PER_ELEMENT, + 3 + ); + return vec3.fromValues(arr[0], arr[1], arr[2]); +}; + +export const getMeshUVAtIndex = (mesh: Mesh, index: number) => { + const arr = new Float32Array( + mesh.vertices.buffer, + index * mesh.vertexStride + 6 * Float32Array.BYTES_PER_ELEMENT, + 2 + ); + return vec2.fromValues(arr[0], arr[1]); +}; diff --git a/src/pages/samples/[slug].tsx b/src/pages/samples/[slug].tsx index 79623cd9..079d501d 100644 --- a/src/pages/samples/[slug].tsx +++ b/src/pages/samples/[slug].tsx @@ -48,6 +48,7 @@ export const pages: PageComponentType = { worker: dynamic(() => import('../../sample/worker/main')), 'A-buffer': dynamic(() => import('../../sample/a-buffer/main')), bitonicSort: dynamic(() => import('../../sample/bitonicSort/main')), + normalMap: dynamic(() => import('../../sample/normalMap/main')), }; function Page({ slug }: Props): JSX.Element { diff --git a/src/sample/normalMap/main.ts b/src/sample/normalMap/main.ts new file mode 100644 index 00000000..624f3a15 --- /dev/null +++ b/src/sample/normalMap/main.ts @@ -0,0 +1,400 @@ +import { mat4 } from 'wgpu-matrix'; +import { makeSample, SampleInit } from '../../components/SampleLayout'; +import normalMapWGSL from './normalMap.wgsl'; +import { createMeshRenderable } from '../../meshes/mesh'; +import { createBoxMeshWithTangents } from '../../meshes/box'; +import { + PBRDescriptor, + createPBRDescriptor, + createBindGroupDescriptor, + create3DRenderPipeline, +} from './utils'; + +const MAT4X4_BYTES = 64; +enum TextureAtlas { + Spiral, + Toybox, + BrickWall, +} + +const init: SampleInit = async ({ canvas, pageState, gui }) => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + if (!pageState.active) return; + const context = canvas.getContext('webgpu') as GPUCanvasContext; + const devicePixelRatio = window.devicePixelRatio; + canvas.width = canvas.clientWidth * devicePixelRatio; + canvas.height = canvas.clientHeight * devicePixelRatio; + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', + }); + + interface GUISettings { + 'Bump Mode': + | 'Diffuse Texture' + | 'Normal Texture' + | 'Depth Texture' + | 'Normal Map' + | 'Parallax Scale' + | 'Steep Parallax'; + cameraPosX: number; + cameraPosY: number; + cameraPosZ: number; + lightPosX: number; + lightPosY: number; + lightPosZ: number; + lightIntensity: number; + depthScale: number; + depthLayers: number; + Texture: string; + 'Reset Light': () => void; + } + + const settings: GUISettings = { + 'Bump Mode': 'Normal Map', + cameraPosX: 0.0, + cameraPosY: 0.8, + cameraPosZ: -1.4, + lightPosX: 1.7, + lightPosY: 0.7, + lightPosZ: -1.9, + lightIntensity: 0.02, + depthScale: 0.05, + depthLayers: 16, + Texture: 'Spiral', + 'Reset Light': () => { + return; + }, + }; + + // Create normal mapping resources and pipeline + const depthTexture = device.createTexture({ + size: [canvas.width, canvas.height], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const uniformBuffer = device.createBuffer({ + // Buffer holding projection, view, and model matrices plus padding bytes + size: MAT4X4_BYTES * 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const mapMethodBuffer = device.createBuffer({ + // Buffer holding mapping type, light uniforms, and depth uniforms + size: Float32Array.BYTES_PER_ELEMENT * 7, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + // Create PBR info (diffuse, normal, and depth/height textures) + let spiralPBR: Required; + { + const response = await createPBRDescriptor(device, [ + 'wood_diffuse.png', + 'spiral_normal.png', + 'spiral_height.png', + ]); + spiralPBR = response as Required; + } + + let toyboxPBR: Required; + { + const response = await createPBRDescriptor(device, [ + 'wood_diffuse.png', + 'toybox_normal.png', + 'toybox_height.png', + ]); + toyboxPBR = response as Required; + } + + let brickWallPBR: Required; + { + const response = await createPBRDescriptor(device, [ + 'brickwall_diffuse.png', + 'brickwall_normal.png', + 'brickwall_height.png', + ]); + brickWallPBR = response as Required; + } + + // Create a sampler with linear filtering for smooth interpolation. + const sampler = device.createSampler({ + magFilter: 'linear', + minFilter: 'linear', + }); + + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: undefined, // Assigned later + + clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + }; + + const box = createMeshRenderable( + device, + createBoxMeshWithTangents(1.0, 1.0, 1.0) + ); + + // Uniform bindGroups and bindGroupLayout + const frameBGDescriptor = createBindGroupDescriptor( + [0, 1], + [ + GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX, + ], + ['buffer', 'buffer'], + [{ type: 'uniform' }, { type: 'uniform' }], + [[{ buffer: uniformBuffer }, { buffer: mapMethodBuffer }]], + 'Frame', + device + ); + + // Texture bindGroups and bindGroupLayout + const surfaceBGDescriptor = createBindGroupDescriptor( + [0, 1, 2, 3], + [GPUShaderStage.FRAGMENT], + ['sampler', 'texture', 'texture', 'texture'], + [ + { type: 'filtering' }, + { sampleType: 'float' }, + { sampleType: 'float' }, + { sampleType: 'float' }, + ], + // Multiple bindgroups that accord to the layout defined above + [ + [ + sampler, + spiralPBR.diffuse.createView(), + spiralPBR.normal.createView(), + spiralPBR.height.createView(), + ], + [ + sampler, + toyboxPBR.diffuse.createView(), + toyboxPBR.normal.createView(), + toyboxPBR.height.createView(), + ], + [ + sampler, + brickWallPBR.diffuse.createView(), + brickWallPBR.normal.createView(), + brickWallPBR.height.createView(), + ], + ], + 'Surface', + device + ); + + const aspect = canvas.width / canvas.height; + const projectionMatrix = mat4.perspective( + (2 * Math.PI) / 5, + aspect, + 0.1, + 10.0 + ) as Float32Array; + + function getViewMatrix() { + return mat4.lookAt( + [settings.cameraPosX, settings.cameraPosY, settings.cameraPosZ], + [0, 0, 0], + [0, 1, 0] + ); + } + + function getModelMatrix() { + const modelMatrix = mat4.create(); + mat4.identity(modelMatrix); + mat4.rotateX(modelMatrix, 10, modelMatrix); + const now = Date.now() / 1000; + mat4.rotateY(modelMatrix, now * -0.5, modelMatrix); + return modelMatrix; + } + + // Change the model mapping type + const getMappingType = (): number => { + switch (settings['Bump Mode']) { + case 'Diffuse Texture': + return 0; + case 'Normal Texture': + return 1; + case 'Depth Texture': + return 2; + case 'Normal Map': + return 3; + case 'Parallax Scale': + return 4; + case 'Steep Parallax': + return 5; + } + }; + + const texturedCubePipeline = create3DRenderPipeline( + device, + 'NormalMappingRender', + [frameBGDescriptor.bindGroupLayout, surfaceBGDescriptor.bindGroupLayout], + normalMapWGSL, + // Position, normal uv tangent bitangent + ['float32x3', 'float32x3', 'float32x2', 'float32x3', 'float32x3'], + normalMapWGSL, + presentationFormat, + true + ); + + let currentSurfaceBindGroup = 0; + const onChangeTexture = () => { + currentSurfaceBindGroup = TextureAtlas[settings.Texture]; + }; + + gui.add(settings, 'Bump Mode', [ + 'Diffuse Texture', + 'Normal Texture', + 'Depth Texture', + 'Normal Map', + 'Parallax Scale', + 'Steep Parallax', + ]); + gui + .add(settings, 'Texture', ['Spiral', 'Toybox', 'BrickWall']) + .onChange(onChangeTexture); + const lightFolder = gui.addFolder('Light'); + const depthFolder = gui.addFolder('Depth'); + lightFolder.add(settings, 'Reset Light').onChange(() => { + lightPosXController.setValue(1.7); + lightPosYController.setValue(-0.7); + lightPosZController.setValue(1.9); + lightIntensityController.setValue(0.02); + }); + const lightPosXController = lightFolder + .add(settings, 'lightPosX', -5, 5) + .step(0.1); + const lightPosYController = lightFolder + .add(settings, 'lightPosY', -5, 5) + .step(0.1); + const lightPosZController = lightFolder + .add(settings, 'lightPosZ', -5, 5) + .step(0.1); + const lightIntensityController = lightFolder + .add(settings, 'lightIntensity', 0.0, 0.1) + .step(0.002); + depthFolder.add(settings, 'depthScale', 0.0, 0.1).step(0.01); + depthFolder.add(settings, 'depthLayers', 1, 32).step(1); + + function frame() { + if (!pageState.active) return; + + // Write to normal map shader + const viewMatrix = getViewMatrix(); + + const modelMatrix = getModelMatrix(); + + const matrices = new Float32Array([ + ...projectionMatrix, + ...viewMatrix, + ...modelMatrix, + ]); + + const mappingType = getMappingType(); + + device.queue.writeBuffer( + uniformBuffer, + 0, + matrices.buffer, + matrices.byteOffset, + matrices.byteLength + ); + + device.queue.writeBuffer( + mapMethodBuffer, + 0, + new Uint32Array([mappingType]) + ); + + device.queue.writeBuffer( + mapMethodBuffer, + 4, + new Float32Array([ + settings.lightPosX, + settings.lightPosY, + settings.lightPosZ, + settings.lightIntensity, + settings.depthScale, + settings.depthLayers, + ]) + ); + + renderPassDescriptor.colorAttachments[0].view = context + .getCurrentTexture() + .createView(); + + const commandEncoder = device.createCommandEncoder(); + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + // Draw textured Cube + passEncoder.setPipeline(texturedCubePipeline); + passEncoder.setBindGroup(0, frameBGDescriptor.bindGroups[0]); + passEncoder.setBindGroup( + 1, + surfaceBGDescriptor.bindGroups[currentSurfaceBindGroup] + ); + passEncoder.setVertexBuffer(0, box.vertexBuffer); + passEncoder.setIndexBuffer(box.indexBuffer, 'uint16'); + passEncoder.drawIndexed(box.indexCount); + passEncoder.end(); + device.queue.submit([commandEncoder.finish()]); + + requestAnimationFrame(frame); + } + requestAnimationFrame(frame); +}; + +const NormalMapping: () => JSX.Element = () => + makeSample({ + name: 'Normal Mapping', + description: + 'This example demonstrates multiple different methods that employ fragment shaders to achieve additional perceptual depth on the surface of a cube mesh. Demonstrated methods include normal mapping, parallax mapping, and steep parallax mapping.', + gui: true, + init, + sources: [ + { + name: __filename.substring(__dirname.length + 1), + contents: __SOURCE__, + }, + { + name: './normalMap.wgsl', + contents: normalMapWGSL, + editable: true, + }, + { + name: '../../meshes/box.ts', + // eslint-disable-next-line @typescript-eslint/no-var-requires + contents: require('!!raw-loader!../../meshes/box.ts').default, + }, + { + name: '../../meshes/mesh.ts', + // eslint-disable-next-line @typescript-eslint/no-var-requires + contents: require('!!raw-loader!../../meshes/mesh.ts').default, + }, + { + name: './utils.ts', + // eslint-disable-next-line @typescript-eslint/no-var-requires + contents: require('!!raw-loader!./utils.ts').default, + }, + ], + filename: __filename, + }); + +export default NormalMapping; diff --git a/src/sample/normalMap/normalMap.wgsl b/src/sample/normalMap/normalMap.wgsl new file mode 100644 index 00000000..12696306 --- /dev/null +++ b/src/sample/normalMap/normalMap.wgsl @@ -0,0 +1,199 @@ +struct SpaceTransformUniforms { + projMatrix: mat4x4f, + viewMatrix: mat4x4f, + modelMatrix: mat4x4f, +} + +struct Uniforms_MapInfo { + mappingType: u32, + lightPosX: f32, + lightPosY: f32, + lightPosZ: f32, + lightIntensity: f32, + depthScale: f32, + depthLayers: f32, +} + +struct VertexInput { + // Shader assumes the missing 4th float is 1.0 + @location(0) position : vec4f, + @location(1) normal : vec3f, + @location(2) uv : vec2f, + @location(3) vert_tan: vec3f, + @location(4) vert_bitan: vec3f, +} + +struct VertexOutput { + @builtin(position) Position : vec4f, + @location(0) normal: vec3f, + @location(1) uv : vec2f, + // Vertex position in world space + @location(2) posWS: vec3f, + // Vertex position in tangent space + @location(3) posTS: vec3f, + // View position in tangent space + @location(4) viewTS: vec3f, + // Extracted components of our tbn matrix + @location(5) tbnTS0: vec3, + @location(6) tbnTS1: vec3, + @location(7) tbnTS2: vec3, +} + +// Uniforms +@group(0) @binding(0) var spaceTransform : SpaceTransformUniforms; +@group(0) @binding(1) var mapInfo: Uniforms_MapInfo; + +// Texture info +@group(1) @binding(0) var textureSampler: sampler; +@group(1) @binding(1) var diffuseTexture: texture_2d; +@group(1) @binding(2) var normalTexture: texture_2d; +@group(1) @binding(3) var depthTexture: texture_2d; + +fn parallax_uv( + uv: vec2f, + viewDirTS: vec3f, + depthSample: f32, + depthScale: f32, +) -> vec2f { + if (mapInfo.mappingType == 4) { + // Perturb uv coordinates based on depth and camera direction + let p = viewDirTS.xy * (depthSample * depthScale) / viewDirTS.z; + return uv - p; + } + // Break up depth space into layers + let depthPerLayer = 1.0 / f32(mapInfo.depthLayers); + // Start at lowest depth + var currentDepth = 0.0; + let delta_uv = viewDirTS.xy * depthScale / (viewDirTS.z * mapInfo.depthLayers); + var prev_uv = uv; + var cur_uv = uv; + + var depthFromTexture = textureSample(depthTexture, textureSampler, cur_uv).r; + var prevDepthFromTexture = depthFromTexture; + var prevCurrentDepth = currentDepth; + for (var i: u32 = 0; i < 32; i++) { + currentDepth += depthPerLayer; + prev_uv = cur_uv; + cur_uv -= delta_uv; + depthFromTexture = textureSample(depthTexture, textureSampler, cur_uv).r; + // Determine whether current depth is greater than depth map + // Once we reach a certain threshold, we stop updating cur_uv + cur_uv = select(cur_uv, prev_uv, depthFromTexture < currentDepth); + prevDepthFromTexture = select(depthFromTexture, prevDepthFromTexture, prevDepthFromTexture < currentDepth); + prevCurrentDepth = select(currentDepth, prevCurrentDepth, prevDepthFromTexture < currentDepth); + } + return cur_uv; +} + +fn when_greater(v1: f32, v2: f32) -> f32 { + return max(sign(v1 - v2), 0.0); +} + +@vertex +fn vertexMain(input: VertexInput) -> VertexOutput { + var output : VertexOutput; + // Create the Model to View Matrix + let MV = spaceTransform.viewMatrix * spaceTransform.modelMatrix; + // Create the Model to View to Projection Matrix + let MVP = spaceTransform.projMatrix * MV; + + // Get Clip space transforms and pass through values out of the way + output.Position = MVP * input.position; + output.uv = input.uv; + output.normal = input.normal; + + // Multiply pos by modelMatrix to get the vertex/fragment's position in world space + output.posWS = vec3f((spaceTransform.modelMatrix * input.position).xyz); + + var MV3x3 = mat3x3f( + MV[0].xyz, + MV[1].xyz, + MV[2].xyz + ); + + // Get unit vectors of normal, tangent, and bitangents in model space + let vertexTangent = normalize(input.vert_tan); + let vertexBitangent = normalize(input.vert_bitan); + let vertexNormal = normalize(input.normal); + + // Convert tbn unit vectors to mv space for a model view tbn + var tbnTS = transpose( + MV3x3 * mat3x3f( + vertexTangent, + vertexBitangent, + vertexNormal + ) + ); + // Condense to vec3s so they can be passed to fragment shader + output.tbnTS0 = tbnTS[0]; + output.tbnTS1 = tbnTS[1]; + output.tbnTS2 = tbnTS[2]; + + // Get the tangent space position of the vertex + output.posTS = tbnTS * (MV * input.position).xyz; + // Get the tangent space position of the camera view + output.viewTS = tbnTS * vec3f(0.0, 0.0, 0.0); + + return output; +} + +@fragment +fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { + // Reconstruct tbnTS + let tbnTS = mat3x3f( + input.tbnTS0, + input.tbnTS1, + input.tbnTS2, + ); + + // Get direction of view in tangent space + let viewDirTS = normalize(input.viewTS - input.posTS); + + // Get position, direction, and distance of light in tangent space (no need to multiply by model matrix as there is no model) + let lightPosVS = spaceTransform.viewMatrix * vec4f(mapInfo.lightPosX, mapInfo.lightPosY, mapInfo.lightPosZ, 1.0); + let lightPosTS = tbnTS * lightPosVS.xyz; + let lightDirTS = normalize(lightPosTS - input.posTS); + let lightDistanceTS = distance(input.posTS, lightPosTS); + + let depthMap = textureSample(depthTexture, textureSampler, input.uv); + + let uv = select( + parallax_uv(input.uv, viewDirTS, depthMap.r, mapInfo.depthScale), + input.uv, + mapInfo.mappingType < 4 + ); + + // Get values from textures + let diffuseMap = textureSample(diffuseTexture, textureSampler, uv); + let normalMap = textureSample(normalTexture, textureSampler, uv); + + // Get normal in tangent space + let normalTS = normalize((normalMap.xyz * 2.0) - 1.0); + + // Calculate diffusion lighting + let lightColorIntensity = vec3f(255.0, 255.0, 255.0) * mapInfo.lightIntensity; + //How similar is the normal to the lightDirection + let diffuseStrength = clamp( + dot(normalTS, lightDirTS), 0.0, 1.0 + ); + // Strenght inversely proportional to square of distance from light + let diffuseLight = (lightColorIntensity * diffuseStrength) / (lightDistanceTS * lightDistanceTS); + + switch (mapInfo.mappingType) { + // Output the diffuse texture + case 0: { + return vec4f(diffuseMap.rgb, 1.0); + } + // Output the normal map + case 1: { + return vec4f(normalMap.rgb, 1.0); + } + // Output the height map + case 2: { + return vec4f(depthMap.rgb, 1.0); + } + default: { + return vec4f(diffuseMap.rgb * diffuseLight, 1.0); + } + } +} \ No newline at end of file diff --git a/src/sample/normalMap/utils.ts b/src/sample/normalMap/utils.ts new file mode 100644 index 00000000..f0742be9 --- /dev/null +++ b/src/sample/normalMap/utils.ts @@ -0,0 +1,267 @@ +type BindGroupBindingLayout = + | GPUBufferBindingLayout + | GPUTextureBindingLayout + | GPUSamplerBindingLayout + | GPUStorageTextureBindingLayout + | GPUExternalTextureBindingLayout; + +export type BindGroupsObjectsAndLayout = { + bindGroups: GPUBindGroup[]; + bindGroupLayout: GPUBindGroupLayout; +}; + +type ResourceTypeName = + | 'buffer' + | 'texture' + | 'sampler' + | 'externalTexture' + | 'storageTexture'; + +/** + * @param {number[]} bindings - The binding value of each resource in the bind group. + * @param {number[]} visibilities - The GPUShaderStage visibility of the resource at the corresponding index. + * @param {ResourceTypeName[]} resourceTypes - The resourceType at the corresponding index. + * @returns {BindGroupsObjectsAndLayout} An object containing an array of bindGroups and the bindGroupLayout they implement. + */ +export const createBindGroupDescriptor = ( + bindings: number[], + visibilities: number[], + resourceTypes: ResourceTypeName[], + resourceLayouts: BindGroupBindingLayout[], + resources: GPUBindingResource[][], + label: string, + device: GPUDevice +): BindGroupsObjectsAndLayout => { + // Create layout of each entry within a bindGroup + const layoutEntries: GPUBindGroupLayoutEntry[] = []; + for (let i = 0; i < bindings.length; i++) { + layoutEntries.push({ + binding: bindings[i], + visibility: visibilities[i % visibilities.length], + [resourceTypes[i]]: resourceLayouts[i], + }); + } + + // Apply entry layouts to bindGroupLayout + const bindGroupLayout = device.createBindGroupLayout({ + label: `${label}.bindGroupLayout`, + entries: layoutEntries, + }); + + // Create bindGroups that conform to the layout + const bindGroups: GPUBindGroup[] = []; + for (let i = 0; i < resources.length; i++) { + const groupEntries: GPUBindGroupEntry[] = []; + for (let j = 0; j < resources[0].length; j++) { + groupEntries.push({ + binding: j, + resource: resources[i][j], + }); + } + const newBindGroup = device.createBindGroup({ + label: `${label}.bindGroup${i}`, + layout: bindGroupLayout, + entries: groupEntries, + }); + bindGroups.push(newBindGroup); + } + + return { + bindGroups, + bindGroupLayout, + }; +}; + +export type ShaderKeyInterface = { + [K in T[number]]: number; +}; + +interface AttribAcc { + attributes: GPUVertexAttribute[]; + arrayStride: number; +} + +/** + * @param {GPUVertexFormat} vf - A valid GPUVertexFormat, representing a per-vertex value that can be passed to the vertex shader. + * @returns {number} The number of bytes present in the value to be passed. + */ +export const convertVertexFormatToBytes = (vf: GPUVertexFormat): number => { + const splitFormat = vf.split('x'); + const bytesPerElement = parseInt(splitFormat[0].replace(/[^0-9]/g, '')) / 8; + + const bytesPerVec = + bytesPerElement * + (splitFormat[1] !== undefined ? parseInt(splitFormat[1]) : 1); + + return bytesPerVec; +}; + +/** Creates a GPUVertexBuffer Layout that maps to an interleaved vertex buffer. + * @param {GPUVertexFormat[]} vertexFormats - An array of valid GPUVertexFormats. + * @returns {GPUVertexBufferLayout} A GPUVertexBufferLayout representing an interleaved vertex buffer. + */ +export const createVBuffer = ( + vertexFormats: GPUVertexFormat[] +): GPUVertexBufferLayout => { + const initialValue: AttribAcc = { attributes: [], arrayStride: 0 }; + + const vertexBuffer = vertexFormats.reduce( + (acc: AttribAcc, curr: GPUVertexFormat, idx: number) => { + const newAttribute: GPUVertexAttribute = { + shaderLocation: idx, + offset: acc.arrayStride, + format: curr, + }; + const nextOffset: number = + acc.arrayStride + convertVertexFormatToBytes(curr); + + const retVal: AttribAcc = { + attributes: [...acc.attributes, newAttribute], + arrayStride: nextOffset, + }; + return retVal; + }, + initialValue + ); + + const layout: GPUVertexBufferLayout = { + arrayStride: vertexBuffer.arrayStride, + attributes: vertexBuffer.attributes, + }; + + return layout; +}; + +export const create3DRenderPipeline = ( + device: GPUDevice, + label: string, + bgLayouts: GPUBindGroupLayout[], + vertexShader: string, + vBufferFormats: GPUVertexFormat[], + fragmentShader: string, + presentationFormat: GPUTextureFormat, + depthTest = false, + topology: GPUPrimitiveTopology = 'triangle-list', + cullMode: GPUCullMode = 'back' +) => { + const pipelineDescriptor: GPURenderPipelineDescriptor = { + label: `${label}.pipeline`, + layout: device.createPipelineLayout({ + label: `${label}.pipelineLayout`, + bindGroupLayouts: bgLayouts, + }), + vertex: { + module: device.createShaderModule({ + label: `${label}.vertexShader`, + code: vertexShader, + }), + entryPoint: 'vertexMain', + buffers: + vBufferFormats.length !== 0 ? [createVBuffer(vBufferFormats)] : [], + }, + fragment: { + module: device.createShaderModule({ + label: `${label}.fragmentShader`, + code: fragmentShader, + }), + entryPoint: 'fragmentMain', + targets: [ + { + format: presentationFormat, + }, + ], + }, + primitive: { + topology: topology, + cullMode: cullMode, + }, + }; + if (depthTest) { + pipelineDescriptor.depthStencil = { + depthCompare: 'less', + depthWriteEnabled: true, + format: 'depth24plus', + }; + } + return device.createRenderPipeline(pipelineDescriptor); +}; + +export const createTextureFromImage = ( + device: GPUDevice, + bitmap: ImageBitmap +) => { + const texture: GPUTexture = device.createTexture({ + size: [bitmap.width, bitmap.height, 1], + format: 'rgba8unorm', + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + device.queue.copyExternalImageToTexture( + { source: bitmap }, + { texture: texture }, + [bitmap.width, bitmap.height] + ); + return texture; +}; + +export interface PBRDescriptor { + diffuse?: GPUTexture; + normal?: GPUTexture; + height?: GPUTexture; +} + +interface URLLoad { + url: string; + type: keyof PBRDescriptor; +} + +export const createPBRDescriptor = async ( + device: GPUDevice, + urls: string[] +): Promise => { + const imgAssetPrepend = '/img/'; + const loads = urls.map((url) => { + const splits = url.split('_'); + const ttype = splits[splits.length - 1].split('.')[0]; + const load: URLLoad = { + url: imgAssetPrepend + url, + type: ttype as keyof PBRDescriptor, + }; + return load; + }); + console.log(loads); + const pbr: PBRDescriptor = {}; + for (let i = 0; i < loads.length; i++) { + console.log(loads[i].url); + console.log(import.meta.url); + let texture: GPUTexture; + { + const response = await fetch(loads[i].url); + const imageBitmap = await createImageBitmap(await response.blob()); + texture = createTextureFromImage(device, imageBitmap); + } + + console.log(loads[i].type); + + switch (loads[i].type) { + case 'diffuse': + { + pbr.diffuse = texture; + } + break; + case 'height': + { + pbr.height = texture; + } + break; + case 'normal': + { + pbr.normal = texture; + } + break; + } + } + return pbr; +};