Skip to content

Commit 9ec5456

Browse files
committed
Add rotating cube example
1 parent a98356a commit 9ec5456

File tree

9 files changed

+259
-38
lines changed

9 files changed

+259
-38
lines changed

packages/shared/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"devDependencies": {
2121
"@repo/eslint-config": "workspace:*",
2222
"@repo/typescript-config": "workspace:*",
23+
"esbuild-plugin-webgl": "workspace:*",
2324
"@testing-library/react": "^16.0.0",
2425
"@types/node": "^20.14.9",
2526
"@types/react": "^18.3.3",
@@ -40,6 +41,7 @@
4041
"@mayank1513/fork-me": "^2.1.2",
4142
"@repo/scripts": "workspace:*",
4243
"esbuild-plugin-webgl": "workspace:*",
44+
"gl-matrix": "^3.4.3",
4345
"nextjs-darkmode": "^1.0.4",
4446
"nextjs-themes": "^4.0.3",
4547
"r18gs": "^1.1.3",
@@ -56,4 +58,4 @@
5658
"optional": true
5759
}
5860
}
59-
}
61+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
precision mediump float;
2+
3+
varying vec3 fragColor;
4+
void main() {
5+
gl_FragColor = vec4(fragColor, 1.0);
6+
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
.rotating-cube {
1+
.cube {
22
/* create your container styles here */
3-
}
3+
height: 400px;
4+
width: 600px;
5+
}
Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
1-
import { HTMLProps, ReactNode } from "react";
1+
import { HTMLProps, useEffect, useRef } from "react";
22
import styles from "./rotating-cube.module.scss";
3-
4-
export interface RotatingCubeProps extends HTMLProps<HTMLDivElement> {
5-
children?: ReactNode;
6-
}
3+
import { createCube } from "./utils";
74

85
/**
9-
*
6+
*
107
*
118
* @example
129
* ```tsx
1310
* <RotatingCube />
1411
* ```
15-
*
12+
*
1613
* @source - Source code
1714
*/
18-
export const RotatingCube = ({ children, ...props }: RotatingCubeProps) => {
19-
const className = [props.className, styles["rotating-cube"]].filter(Boolean).join(" ");
20-
return (
21-
<div {...props} className={className} data-testid="rotating-cube">
22-
{children}
23-
</div>
24-
);
25-
}
15+
export const RotatingCube = ({ className, ...props }: HTMLProps<HTMLCanvasElement>) => {
16+
const canvasRef = useRef<HTMLCanvasElement>(null);
17+
useEffect(() => {
18+
if (!canvasRef.current) return;
19+
const gl = canvasRef.current.getContext("webgl");
20+
if (!gl) {
21+
// skipcq: JS-0052 -- Alert required
22+
alert("Your browser does not support WebGL");
23+
return;
24+
}
25+
createCube(canvasRef.current, gl);
26+
}, []);
27+
return <canvas className={[className, styles.cube].join(" ")} ref={canvasRef} {...props} />;
28+
};
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import vertexShaderText from "./vertex.glsl?raw";
2+
import fragmentShaderText from "./fragment.glsl?raw";
3+
import { mat4, glMatrix } from "gl-matrix";
4+
5+
/** Create rotating Cube */
6+
export const createCube = (canvas: HTMLCanvasElement, gl: WebGLRenderingContext) => {
7+
gl.clearColor(2.55, 2.55, 2.55, 1);
8+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
9+
gl.enable(gl.DEPTH_TEST);
10+
gl.enable(gl.CULL_FACE);
11+
gl.frontFace(gl.CCW);
12+
gl.cullFace(gl.BACK);
13+
14+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
15+
16+
/** Create shader */
17+
const createShader = (type: number, source: string): WebGLShader => {
18+
const shader = gl.createShader(type);
19+
if (!shader) throw new Error("Failed to create shader");
20+
gl.shaderSource(shader, source);
21+
gl.compileShader(shader);
22+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
23+
/* v8 ignore next */
24+
const info = gl.getShaderInfoLog(shader);
25+
/* v8 ignore next */
26+
gl.deleteShader(shader);
27+
/* v8 ignore next */
28+
throw new Error("Could not compile WebGL shader. \n\n" + info); // skipcq: JS-0246
29+
/* v8 ignore next */
30+
}
31+
return shader;
32+
};
33+
34+
/** Create webGL program */
35+
const createProgram = (
36+
vertexShaderSource: string,
37+
fragmentShaderSource: string,
38+
): WebGLProgram => {
39+
const vertexShader = createShader(gl.VERTEX_SHADER, vertexShaderSource);
40+
const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentShaderSource);
41+
const program = gl.createProgram();
42+
if (!program) throw new Error("Failed to create program");
43+
gl.attachShader(program, vertexShader);
44+
gl.attachShader(program, fragmentShader);
45+
46+
gl.linkProgram(program);
47+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
48+
/* v8 ignore next */
49+
// eslint-disable-next-line no-console -- error handling
50+
console.error(gl.getProgramInfoLog(program));
51+
/* v8 ignore next */
52+
throw new Error("Failed to link program");
53+
/* v8 ignore next */
54+
}
55+
return program;
56+
};
57+
58+
const rotatingCubeProgram = createProgram(vertexShaderText, fragmentShaderText);
59+
60+
//Buffers
61+
62+
const boxVertices = [
63+
//top
64+
-1.0, 1.0, -1.0, 0.5, 0.5, 0.5, -1.0, 1.0, 1.0, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 0.5, 0.5, 0.5,
65+
1.0, 1.0, -1.0, 0.5, 0.5, 0.5,
66+
//left
67+
-1.0, 1.0, 1.0, 0.75, 0.25, 0.5, -1.0, -1.0, 1.0, 0.75, 0.25, 0.5, -1.0, -1.0, -1.0, 0.75, 0.25,
68+
0.5, -1.0, 1.0, -1.0, 0.75, 0.25, 0.5,
69+
//right
70+
1.0, 1.0, 1.0, 0.25, 0.25, 0.75, 1.0, -1.0, 1.0, 0.25, 0.25, 0.75, 1.0, -1.0, -1.0, 0.25, 0.25,
71+
0.75, 1.0, 1.0, -1.0, 0.25, 0.25, 0.75,
72+
//front
73+
1.0, 1.0, 1.0, 1.0, 0.0, 0.15, 1.0, -1.0, 1.0, 1.0, 0.0, 0.15, -1.0, -1.0, 1.0, 1.0, 0.0, 0.15,
74+
-1.0, 1.0, 1.0, 1.0, 0.0, 0.15,
75+
//back
76+
1.0, 1.0, -1.0, 0.0, 1.0, 0.15, 1.0, -1.0, -1.0, 0.0, 1.0, 0.15, -1.0, -1.0, -1.0, 0.0, 1.0,
77+
0.15, -1.0, 1.0, -1.0, 0.0, 1.0, 0.15,
78+
//bottom
79+
-1.0, -1.0, -1.0, 0.5, 0.5, 1.0, -1.0, -1.0, 1.0, 0.5, 0.5, 1.0, 1.0, -1.0, 1.0, 0.5, 0.5, 1.0,
80+
1.0, -1.0, -1.0, 0.5, 0.5, 1.0,
81+
];
82+
83+
const boxIndices = [
84+
//top
85+
0, 1, 2, 0, 2, 3,
86+
//left
87+
5, 4, 6, 6, 4, 7,
88+
// right
89+
8, 9, 10, 8, 10, 11,
90+
//front
91+
13, 12, 14, 15, 14, 12,
92+
//back
93+
16, 17, 18, 16, 18, 19,
94+
//bottom
95+
21, 20, 22, 22, 20, 23,
96+
];
97+
98+
const boxVertexBufferObject = gl.createBuffer();
99+
gl.bindBuffer(gl.ARRAY_BUFFER, boxVertexBufferObject);
100+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxVertices), gl.STATIC_DRAW);
101+
102+
const boxIndexBufferObject = gl.createBuffer();
103+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, boxIndexBufferObject);
104+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(boxIndices), gl.STATIC_DRAW);
105+
106+
const positionAttribLocation = gl.getAttribLocation(rotatingCubeProgram, "vertPosition");
107+
const colorAttribLocation = gl.getAttribLocation(rotatingCubeProgram, "vertColor");
108+
109+
gl.vertexAttribPointer(
110+
positionAttribLocation,
111+
3,
112+
gl.FLOAT,
113+
false,
114+
6 * Float32Array.BYTES_PER_ELEMENT,
115+
0,
116+
);
117+
gl.vertexAttribPointer(
118+
colorAttribLocation,
119+
3,
120+
gl.FLOAT,
121+
false,
122+
6 * Float32Array.BYTES_PER_ELEMENT,
123+
3 * Float32Array.BYTES_PER_ELEMENT,
124+
);
125+
gl.enableVertexAttribArray(positionAttribLocation);
126+
gl.enableVertexAttribArray(colorAttribLocation);
127+
128+
//Program used
129+
gl.useProgram(rotatingCubeProgram);
130+
131+
const matWorldUniformLocation = gl.getUniformLocation(rotatingCubeProgram, "mWorld");
132+
const matViewUniformLocation = gl.getUniformLocation(rotatingCubeProgram, "mView");
133+
const matProjUniformLocation = gl.getUniformLocation(rotatingCubeProgram, "mProj");
134+
135+
const worldMatrix = new Float32Array(16);
136+
const viewMatrix = new Float32Array(16);
137+
const projMatrix = new Float32Array(16);
138+
139+
mat4.identity(worldMatrix);
140+
mat4.lookAt(viewMatrix, [0, 0, -8], [0, 0, 0], [0, 1, 0]);
141+
mat4.perspective(projMatrix, glMatrix.toRadian(45), canvas.width / canvas.height, 0.1, 1000.0);
142+
143+
gl.uniformMatrix4fv(matWorldUniformLocation, false, worldMatrix);
144+
gl.uniformMatrix4fv(matViewUniformLocation, false, viewMatrix);
145+
gl.uniformMatrix4fv(matProjUniformLocation, false, projMatrix);
146+
147+
const xRotationMatrix = new Float32Array(16);
148+
const yRotationMatrix = new Float32Array(16);
149+
150+
//Render loop
151+
152+
const identityMatrix = new Float32Array(16);
153+
mat4.identity(identityMatrix);
154+
155+
let angle = 0;
156+
157+
/** Render loop */
158+
const loop = function () {
159+
angle = (performance.now() / 1000 / 6) * 2 * Math.PI;
160+
mat4.rotate(yRotationMatrix, identityMatrix, angle, [0, 1, 0]);
161+
mat4.rotate(xRotationMatrix, identityMatrix, angle / 4, [1, 0, 0]);
162+
mat4.mul(worldMatrix, yRotationMatrix, xRotationMatrix);
163+
gl.uniformMatrix4fv(matWorldUniformLocation, false, worldMatrix);
164+
165+
gl.clearColor(2.55, 2.55, 2.55, 1.0);
166+
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT);
167+
168+
gl.drawElements(gl.TRIANGLES, boxIndices.length, gl.UNSIGNED_SHORT, 0);
169+
170+
requestAnimationFrame(loop);
171+
};
172+
173+
requestAnimationFrame(loop);
174+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
precision mediump float;
2+
3+
attribute vec3 vertPosition;
4+
attribute vec3 vertColor;
5+
varying vec3 fragColor;
6+
uniform mat4 mWorld;
7+
uniform mat4 mView;
8+
uniform mat4 mProj;
9+
10+
void main() {
11+
fragColor = vertColor;
12+
gl_Position = mProj * mView * mWorld * vec4(vertPosition, 1.0);
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
declare module "*.module.css";
22
declare module "*.module.scss";
3+
declare module "*.glsl" {
4+
const value: string;
5+
export default value;
6+
}
7+
declare module "*.glsl?raw" {
8+
const value: string;
9+
export default value;
10+
}

packages/shared/tsup.config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { defineConfig, type Options } from "tsup";
22
import react18Plugin from "esbuild-plugin-react18";
33
import cssPlugin from "esbuild-plugin-react18-css";
4+
import { webglPlugin } from "esbuild-plugin-webgl";
45

56
export default defineConfig(
67
(options: Options) =>
@@ -12,7 +13,11 @@ export default defineConfig(
1213
clean: !options.watch,
1314
bundle: true,
1415
minify: !options.watch,
15-
esbuildPlugins: [react18Plugin(), cssPlugin({ generateScopedName: "[folder]__[local]" })],
16+
esbuildPlugins: [
17+
webglPlugin(),
18+
react18Plugin(),
19+
cssPlugin({ generateScopedName: "[folder]__[local]" }),
20+
],
1621
external: ["react"],
1722
...options,
1823
}) as Options,

0 commit comments

Comments
 (0)