Skip to content

Commit f1363c2

Browse files
Remi van der LaanRemi van der Laan
Remi van der Laan
authored and
Remi van der Laan
committed
Refactoring (proper renderer class, linting)
1 parent c071825 commit f1363c2

11 files changed

+1216
-491
lines changed

.eslintrc.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module.exports = {
2+
root: true,
3+
parser: '@typescript-eslint/parser',
4+
plugins: [
5+
'@typescript-eslint',
6+
],
7+
extends: [
8+
'eslint:recommended',
9+
'plugin:@typescript-eslint/eslint-recommended',
10+
'plugin:@typescript-eslint/recommended',
11+
],
12+
rules: {
13+
'@typescript-eslint/interface-name-prefix' : 'off',
14+
'@typescript-eslint/camelcase' : 'off',
15+
'@typescript-eslint/explicit-function-return-type': 'off',
16+
'no-constant-condition': ['error', { checkLoops: false }],
17+
}
18+
};

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
},
2020
"devDependencies": {
2121
"@types/gl-matrix": "^2.4.5",
22+
"@typescript-eslint/eslint-plugin": "^2.34.0",
23+
"@typescript-eslint/parser": "^2.34.0",
24+
"eslint": "^7.0.0",
2225
"gh-pages": "^2.2.0",
2326
"ts-loader": "^7.0.3",
2427
"typescript": "^3.8.3",

src/Renderer.ts

+232
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import Camera from './Camera';
2+
import { SVDAG } from './SVDAG'
3+
import { loadProgram } from './ShaderUtils';
4+
5+
// Coupled to the glsl shader
6+
const UNIFORMS = [
7+
'time', 'resolution',
8+
'viewMatInv', 'projMatInv',
9+
'sceneBBoxMin', 'sceneBBoxMax', 'sceneCenter', 'rootHalfSide',
10+
'maxIters', 'drawLevel', 'projectionFactor',
11+
'nodes',
12+
'viewerRenderMode',
13+
'selectedVoxelIndex', 'uniqueColors',
14+
'lightPos', 'enableShadows', 'normalEpsilon',
15+
] as const;
16+
type Uniform = typeof UNIFORMS[number];
17+
18+
// Dict of uniform name to its ID
19+
type UniformDict = {
20+
[T in Uniform]: WebGLUniformLocation;
21+
}
22+
23+
export enum RenderMode {
24+
ITERATIONS = 0,
25+
DEPTH = 1,
26+
DIFFUSE_LIGHTING = 2,
27+
PATH_TRACING = 3,
28+
}
29+
30+
export interface IRendererState {
31+
renderMode: RenderMode;
32+
startTime: number;
33+
time: number;
34+
frame: number;
35+
pixelTolerance: number;
36+
renderScale: number;
37+
drawLevel: number;
38+
maxIterations: number;
39+
useMinDepthOptimization: boolean;
40+
showUniqueNodeColors: boolean;
41+
}
42+
43+
export default class Renderer {
44+
gl: WebGL2RenderingContext;
45+
uniformDict: UniformDict;
46+
svdag: SVDAG;
47+
48+
program: WebGLProgram;
49+
fragShader: WebGLShader;
50+
texture: WebGLTexture;
51+
// controller: OrbitController;
52+
53+
maxT3DTexels: number;
54+
55+
minDepthTexId?: number;
56+
fullDepthTexId?: number;
57+
58+
state: IRendererState = {
59+
startTime: new Date().getTime() / 1000,
60+
time: 0,
61+
frame: 0,
62+
pixelTolerance: 1,
63+
renderScale: 1,
64+
drawLevel: 1,
65+
maxIterations: 250,
66+
renderMode: RenderMode.ITERATIONS,
67+
useMinDepthOptimization: true,
68+
showUniqueNodeColors: false,
69+
};
70+
71+
constructor(
72+
public camera: Camera,
73+
public canvas: HTMLCanvasElement,
74+
) {
75+
this.gl = canvas.getContext("webgl2") as WebGL2RenderingContext;
76+
this.maxT3DTexels = this.gl.getParameter(this.gl.MAX_3D_TEXTURE_SIZE);
77+
}
78+
79+
public render() {
80+
const { gl, canvas, state } = this;
81+
82+
// TODO: Only update uniforms when they change, not all of them every time
83+
this.setInitialUniforms();
84+
85+
// Render
86+
gl.clear(gl.COLOR_BUFFER_BIT);
87+
gl.viewport(0, 0, canvas.width, canvas.height);
88+
gl.drawArrays(gl.TRIANGLES, 0, 3);
89+
90+
state.frame++;
91+
}
92+
93+
initScene(svdag: SVDAG) {
94+
this.svdag = svdag;
95+
}
96+
97+
async initShaders() {
98+
const { gl, svdag } = this;
99+
[this.program, this.fragShader] = await loadProgram(gl, svdag.nLevels);
100+
}
101+
102+
//////////////////////////////////////////////////////////////
103+
////////////////////////// UNIFORMS //////////////////////////
104+
//////////////////////////////////////////////////////////////
105+
initUniforms() {
106+
const { gl } = this;
107+
this.uniformDict = {} as any;
108+
UNIFORMS.forEach(u => this.uniformDict[u] = gl.getUniformLocation(this.program, u));
109+
}
110+
111+
getProjectionFactor(pixelTolerance: number, screenDivisor: number) {
112+
const { canvas, camera } = this;
113+
const inv_2tan_half_fovy = 1.0 / (2.0 * Math.tan(0.5 * camera.fovY));
114+
const screen_tolerance = pixelTolerance / (canvas.height / screenDivisor);
115+
return inv_2tan_half_fovy / screen_tolerance;
116+
}
117+
118+
setInitialUniforms() {
119+
const { canvas, gl, camera, uniformDict: ud, svdag, state } = this;
120+
if (this.program === undefined) return;
121+
122+
gl.uniform2f(ud.resolution, canvas.width, canvas.height);
123+
124+
gl.uniform1i(ud.nodes, 0);
125+
126+
gl.uniform3fv(ud.sceneBBoxMin, svdag.bboxStart);
127+
gl.uniform3fv(ud.sceneBBoxMax, svdag.bboxEnd);
128+
gl.uniform3fv(ud.sceneCenter, svdag.bboxCenter);
129+
gl.uniform1f(ud.rootHalfSide, svdag.rootSide / 2.0);
130+
131+
// TODO: Make light pos configurable, currently always bboxEnd
132+
gl.uniform3fv(ud.lightPos, svdag.bboxEnd);
133+
134+
gl.uniform1ui(ud.maxIters, state.maxIterations);
135+
gl.uniform1ui(ud.drawLevel, state.drawLevel);
136+
gl.uniform1f(ud.projectionFactor, this.getProjectionFactor(state.pixelTolerance, 1));
137+
138+
gl.uniform1i(ud.uniqueColors, state.showUniqueNodeColors ? 1 : 0);
139+
140+
gl.uniform1i(ud.viewerRenderMode, state.renderMode);
141+
142+
gl.uniformMatrix4fv(ud.viewMatInv, false, camera.viewMatInv);
143+
gl.uniformMatrix4fv(ud.projMatInv, false, camera.projMatInv);
144+
145+
// Todo: make this a vec4 like shadertoy
146+
gl.uniform1f(ud.time, new Date().getTime() / 1000 - state.startTime);
147+
}
148+
149+
// setFrameUniforms() {
150+
151+
// }
152+
153+
154+
//////////////////////////////////////////////////////////////
155+
////////////////////// 3D TEXTURE DATA ///////////////////////
156+
//////////////////////////////////////////////////////////////
157+
createNodesTexture() {
158+
const { gl } = this;
159+
this.texture = gl.createTexture();
160+
gl.activeTexture(gl.TEXTURE0);
161+
gl.bindTexture(gl.TEXTURE_3D, this.texture);
162+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
163+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
164+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.REPEAT);
165+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.REPEAT);
166+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.REPEAT);
167+
}
168+
169+
deleteNodesTexture() {
170+
if (this.texture !== undefined) {
171+
this.gl.deleteTexture(this.texture);
172+
}
173+
}
174+
175+
/**
176+
* Updates the 3D texture with only the nodes that have been loaded since the last texture update
177+
*/
178+
uploadTexData(nNewNodes: number) {
179+
const { gl, svdag, maxT3DTexels } = this;
180+
const maxT3DTexelsPow2 = maxT3DTexels * maxT3DTexels;
181+
const neededTexels = svdag.nodes.length;
182+
const depthLayers = Math.ceil(neededTexels / maxT3DTexelsPow2);
183+
const nTexelsToAllocate = maxT3DTexelsPow2 * depthLayers;
184+
185+
const chunkStart = svdag.dataLoadedOffset / 4 - nNewNodes;
186+
console.log(chunkStart, svdag.dataLoadedOffset / 4, nNewNodes);
187+
188+
if (chunkStart === 0) {
189+
// For the first chunk, define the texture type and upload what data we have
190+
console.log(`Initial uploading of nodes to 3D texture (Resolution: ${maxT3DTexels} x ${maxT3DTexels} x ${depthLayers}, nNodes: ${svdag.dataLoadedOffset}/${svdag.nodes.length})...`);
191+
192+
// Pad the data to fit the 3D texture dimensions (not required but makes it a bit easier)
193+
if (svdag.nodes.length != nTexelsToAllocate) {
194+
console.log('Resizing node buffer from ', svdag.nodes.length, 'to', nTexelsToAllocate, '(3D Texture padding)');
195+
const paddedNodes = new Uint32Array(nTexelsToAllocate);
196+
paddedNodes.set(svdag.nodes);
197+
svdag.nodes = paddedNodes;
198+
}
199+
200+
// Every "pixel" in the 3D texture will be used as a 32 bit int, so we set the type to a single 32 bit red pixel (R32UI)
201+
gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R32UI, maxT3DTexels, maxT3DTexels, depthLayers);
202+
// For the initial load, we'll load all of the data
203+
gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, maxT3DTexels, maxT3DTexels, depthLayers, gl.RED_INTEGER, gl.UNSIGNED_INT, svdag.nodes);
204+
205+
// gl.texImage3D(gl.TEXTURE_3D, 0, gl.R32UI, maxT3DTexels, maxT3DTexels, depthLayers, 0, gl.RED_INTEGER, gl.UNSIGNED_INT, svdag.nodes);
206+
} else {
207+
// For following chunks of data, upload one or more Z slices of the 3D texture
208+
// TODO: Also take into account the yOffset - may need two texSubImage3D calls
209+
const xOffset = 0;
210+
const yOffset = 0; // Math.floor(chunkStart / maxT3DTexels) % maxT3DTexels;
211+
const zOffset = Math.floor(chunkStart / maxT3DTexelsPow2);
212+
213+
const chunkStartZ = zOffset;
214+
const chunkEndZ = Math.floor((svdag.dataLoadedOffset / 4) / maxT3DTexelsPow2);
215+
216+
const updateWidth = maxT3DTexels;
217+
const updateHeight = maxT3DTexels; // Math.ceil(svdag.dataLoadedOffset / maxT3DTexels) % maxT3DTexels - yOffset;
218+
const updateDepth = (chunkEndZ - chunkStartZ) + 1;
219+
220+
const newNodes = svdag.nodes.slice(
221+
yOffset * maxT3DTexels + chunkStartZ * maxT3DTexelsPow2,
222+
maxT3DTexels + updateHeight * maxT3DTexels + chunkEndZ * maxT3DTexelsPow2,
223+
);
224+
225+
const progressPct = Math.round((svdag.dataLoadedOffset / 4) / svdag.originalNodeLength * 100);
226+
console.log(`Uploading data to 3D texture (${svdag.dataLoadedOffset / 4}/${svdag.originalNodeLength} [${progressPct}%])...`);
227+
console.log('#layers', depthLayers, 'start', zOffset, 'chunkStart', chunkStart, 'newNodes', nNewNodes, 'end', chunkEndZ, 'depth', updateDepth)
228+
gl.texSubImage3D(gl.TEXTURE_3D, 0, xOffset, yOffset, zOffset, updateWidth, updateHeight, updateDepth, gl.RED_INTEGER, gl.UNSIGNED_INT, newNodes);
229+
}
230+
}
231+
232+
}

src/SVDAG.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { vec3, quat, mat4 } from 'gl-matrix';
22

33
import Camera from './Camera';
4-
import { IRendererState } from './main';
4+
import { IRendererState } from './Renderer';
55

66
function bitCount(num: number) {
77
let n = num;
@@ -27,15 +27,15 @@ export class SVDAG extends EncodedOctree {
2727
// Since the `nodes` array might get padded, store the original amount of data in here
2828
originalNodeLength: number;
2929

30-
initialized: boolean = false;
30+
initialized = false;
3131
// byte offset (not 32 bit ints, but bytes)
32-
dataLoadedOffset: number = 0;
32+
dataLoadedOffset = 0;
3333

34-
renderPreferences: Partial<IRendererState & { spawnPosition: vec3 }> = {};
34+
renderPreferences: Partial<IRendererState & { spawnPosition: vec3; moveSpeed: number }> = {};
3535

3636
load(buffer: ArrayBuffer) {
3737
// Header
38-
let i = this.parseHeader(buffer);
38+
const i = this.parseHeader(buffer);
3939

4040
// Nodes
4141
this.nodes.set(new Uint32Array(buffer.slice(i)));
@@ -89,7 +89,7 @@ export class SVDAG extends EncodedOctree {
8989
}
9090
}
9191

92-
castRay(o: vec3, d: vec3, maxIters = 100): { nodeIndex: number, hitPos: vec3, maxRayLength: number } | null {
92+
castRay(o: vec3, d: vec3, maxIters = 100): { nodeIndex: number; hitPos: vec3; maxRayLength: number } | null {
9393
// const stack: number[] = []; // TODO: Reuse list of traversed node indices per level
9494

9595
// TODO: Find intersection of ray with next node instead of naively stepping with a small step size
@@ -106,7 +106,7 @@ export class SVDAG extends EncodedOctree {
106106
return null;
107107
}
108108

109-
getVoxel(pos: vec3): { nodeIndex: number, hitPos: vec3 } | null {
109+
getVoxel(pos: vec3): { nodeIndex: number; hitPos: vec3 } | null {
110110
// Transform world position to the [0, 1] range
111111
// const gridPos = vec3.scale(vec3.create(), pos, 1 / (2 * this.rootSide));
112112
const nodeCenter = vec3.create();

src/SceneProvider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { SVDAG } from "./SVDAG";
22
import { vec3 } from "gl-matrix";
3-
import { RenderMode } from "./main";
3+
import { RenderMode } from "./Renderer";
44

55
export type PreloadedSceneOption = {
66
label: string;

src/ShaderUtils.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
function loadVertShader(gl: WebGL2RenderingContext) {
2+
// // https://www.saschawillems.de/blog/2016/08/13/vulkan-tutorial-on-rendering-a-fullscreen-quad-without-buffers/
3+
const vertSrc = `#version 300 es
4+
uniform float time;
5+
void main(void) {
6+
vec2 outUV = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
7+
outUV = outUV * 2.0f - 1.0f;
8+
// gl_Position = vec4(outUV * (1.0 / time) + vec2(cos(time * time), sin(time * time)), 0.0f, 1.0f);
9+
gl_Position = vec4(outUV, 0.0f, 1.0f);
10+
}
11+
`;
12+
const vertShader = gl.createShader(gl.VERTEX_SHADER);
13+
gl.shaderSource(vertShader, vertSrc);
14+
gl.compileShader(vertShader);
15+
console.log('Vert shader: ', gl.getShaderInfoLog(vertShader) || 'OK');
16+
return vertShader;
17+
}
18+
19+
async function loadFragShader(gl: WebGL2RenderingContext, nLevels: number) {
20+
const maxT3DTexels = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE);
21+
const maxT3DTexelsPow2 = maxT3DTexels * maxT3DTexels;
22+
23+
const shaderSrcRes = await fetch('raycast.glsl');
24+
// const shaderSrcRes = await fetch('raycast2.glsl');
25+
let shaderSrc = await shaderSrcRes.text();
26+
27+
const defines = `#version 300 es
28+
#define INNER_LEVELS ${nLevels *2 - 1}u
29+
#define TEX3D_SIZE ${maxT3DTexels}
30+
#define TEX3D_SIZE_POW2 ${maxT3DTexelsPow2}
31+
`;
32+
33+
// Replace the first few lines from shaderSrc with defines
34+
shaderSrc = defines + '\n' + shaderSrc.split('\n').splice(defines.split('\n').length).join('\n');
35+
36+
const shader = gl.createShader(gl.FRAGMENT_SHADER);
37+
gl.shaderSource(shader, shaderSrc);
38+
gl.compileShader(shader);
39+
console.log('Raycast shader: ', gl.getShaderInfoLog(shader) || 'OK');
40+
return shader;
41+
}
42+
43+
export async function loadProgram(gl: WebGL2RenderingContext, nLevels: number) {
44+
// Setup shaders
45+
const vertShader = loadVertShader(gl);
46+
const fragShader = await loadFragShader(gl, nLevels);
47+
48+
// Proper error is printed when we don't check for errors
49+
// if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)
50+
// || !gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) {
51+
// throw new Error('Shader compilation failure');
52+
// }
53+
54+
const program = gl.createProgram();
55+
gl.attachShader(program, vertShader);
56+
gl.attachShader(program, fragShader);
57+
gl.linkProgram(program);
58+
59+
// gl.validateProgram(program);
60+
// More descriptive error is given without validateProgram o.o
61+
62+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
63+
throw 'Could not compile WebGL program. \n\n' + gl.getProgramInfoLog(program);
64+
}
65+
66+
gl.useProgram(program);
67+
68+
return [program, fragShader];
69+
}

src/UIPanel.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// TODO: Proper 2-way binding of state with UI
2+
3+
console.log('Hello world');

0 commit comments

Comments
 (0)