<wc-geo-gl></wc-geo-gl>
const cube = {
positions: [
//Front
-0.5, -0.5, -0.5,
0.5, -0.5, -0.5,
0.5, 0.5, -0.5,
-0.5, 0.5, -0.5,
//Back
0.5, -0.5, 0.5,
-0.5, -0.5, 0.5,
-0.5, 0.5, 0.5,
0.5, 0.5, 0.5
],
colors: [
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0
],
triangles: [
0, 1, 2, //front
0, 2, 3,
1, 4, 7, //right
1, 7, 2,
4, 5, 6, //back
4, 6, 7,
5, 0, 3, //left
5, 3, 6,
3, 2, 7, //top
3, 7, 6,
0, 4, 1, //bottom
0, 5, 4
]
};
const quadPyramid = {
positions: [
0.0, 0.5, 0.0,
-0.5, -0.5, -0.5,
0.5, -0.5, -0.5,
0.5, -0.5, 0.5,
-0.5, -0.5, 0.5
],
colors: [
1.0, 0, 0,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1
],
triangles: [
0, 1, 2,
0, 2, 3,
0, 3, 4,
0, 4, 1
]
}
class Mesh {
#positions;
#colors;
#normals;
#uvs;
#triangles;
#translation = [0, 0, 0];
#scale = [1, 1, 1];
#rotation = [0, 0, 0];
constructor(mesh){
this.positions = mesh.positions;
this.colors = mesh.colors;
this.normals = mesh.normals;
this.uvs = mesh.uvs;
this.triangles = mesh.triangles;
}
set positions(val){
this.#positions = new Float32Array(val);
}
get positions(){
return this.#positions;
}
set colors(val) {
this.#colors = new Float32Array(val);
}
get colors(){
return this.#colors;
}
set normals(val) {
this.#normals = new Float32Array(val);
}
get normals(){
return this.#normals;
}
set uvs(val) {
this.#uvs = new Float32Array(val);
}
get uvs(){
return this.#uvs;
}
set triangles(val) {
this.#triangles = new Uint16Array(val);
}
get triangles(){
return this.#triangles;
}
setTranslation({ x, y, z }){
if (x){
this.#translation[0] = x;
}
if (y) {
this.#translation[1] = y;
}
if (z) {
this.#translation[2] = z;
}
}
getTranslation(){
return this.#translation;
}
setScale({ x, y, z }) {
if (x) {
this.#scale[0] = x;
}
if (y) {
this.#scale[1] = y;
}
if (z) {
this.#scale[2] = z;
}
}
getScale() {
return this.#scale;
}
setRotation({ x, y, z }) {
if (x) {
this.#rotation[0] = x;
}
if (y) {
this.#rotation[1] = y;
}
if (z) {
this.#rotation[2] = z;
}
}
getRotation(){
return this.#rotation;
}
getModelMatrix(){
return new Float32Array(transpose(multiplyMatrix(
getTranslationMatrix(this.#translation[0], this.#translation[1], this.#translation[2]),
multiplyMatrix(
getRotationXMatrix(this.#rotation[0]),
multiplyMatrix(
getRotationYMatrix(this.#rotation[1]),
multiplyMatrix(
getRotationZMatrix(this.#rotation[2]),
multiplyMatrix(
getScaleMatrix(this.#scale[0], this.#scale[1], this.#scale[2]),
getIdentityMatrix()
)
)
)
)
)).flat());
}
}
function getRotationXMatrix(theta) {
return [
[1, 0, 0, 0],
[0, Math.cos(theta), -Math.sin(theta), 0],
[0, Math.sin(theta), Math.cos(theta), 0],
[0, 0, 0, 1]
];
}
function getRotationYMatrix(theta) {
return [
[Math.cos(theta), 0, Math.sin(theta), 0],
[0, 1, 0, 0],
[-Math.sin(theta), 0, Math.cos(theta), 0],
[0, 0, 0, 1]
];
}
function getRotationZMatrix(theta) {
return [
[Math.cos(theta), -Math.sin(theta), 0, 0],
[Math.sin(theta), Math.cos(theta), 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
}
function getTranslationMatrix(x, y, z) {
return [
[1, 0, 0, x],
[0, 1, 0, y],
[0, 0, 1, z],
[0, 0, 0, 1]
];
}
function getScaleMatrix(x, y, z){
return [
[x, 0, 0, 0],
[0, y, 0, 0],
[0, 0, z, 0],
[0, 0, 0, 1]
];
}
function multiplyMatrix(a, b) {
const matrix = [
new Array(4),
new Array(4),
new Array(4),
new Array(4)
];
for (let c = 0; c < 4; c++) {
for (let r = 0; r < 4; r++) {
matrix[r][c] = a[r][0] * b[0][c] + a[r][1] * b[1][c] + a[r][2] * b[2][c] + a[r][3] * b[3][c];
}
}
return matrix;
}
function getIdentityMatrix() {
return [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
}
function getProjectionMatrix(screenHeight, screenWidth, fieldOfView, zNear, zFar) {
const aspectRatio = screenHeight / screenWidth;
const fieldOfViewRadians = fieldOfView * (Math.PI / 180);
const fovRatio = 1 / Math.tan(fieldOfViewRadians / 2);
return [
[aspectRatio * fovRatio, 0, 0, 0],
[0, fovRatio, 0, 0],
[0, 0, zFar / (zFar - zNear), 1],
[0, 0, (-zFar * zNear) / (zFar - zNear), 0]
];
}
function transpose(matrix){
return [
[matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0]],
[matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1]],
[matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2]],
[matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3]]
];
}
function compileShader(context, text, type){
const shader = context.createShader(type);
context.shaderSource(shader, text);
context.compileShader(shader);
if (!context.getShaderParameter(shader, context.COMPILE_STATUS)) {
throw new Error(`Failed to compile shader: ${context.getShaderInfoLog(shader)}`);
}
return shader;
}
function compileProgram(context, vertexShader, fragmentShader){
const program = context.createProgram();
context.attachShader(program, vertexShader);
context.attachShader(program, fragmentShader);
context.linkProgram(program);
if (!context.getProgramParameter(program, context.LINK_STATUS)) {
throw new Error(`Failed to compile WebGL program: ${context.getProgramInfoLog(program)}`);
}
return program;
}
class WcGeoGl extends HTMLElement {
static observedAttributes = ["image", "height", "width"];
#height = 720;
#width = 1280;
#image;
constructor() {
super();
this.bind(this);
}
bind(element) {
element.attachEvents = element.attachEvents.bind(element);
element.cacheDom = element.cacheDom.bind(element);
element.createShadowDom = element.createShadowDom.bind(element);
element.bootGpu = element.bootGpu.bind(element);
element.render = element.render.bind(element);
}
async connectedCallback() {
this.createShadowDom();
this.cacheDom();
this.attachEvents();
await this.bootGpu();
this.createMeshes();
this.setupGlobalUniforms();
this.render();
}
createShadowDom() {
this.shadow = this.attachShadow({ mode: "open" });
this.shadow.innerHTML = `
<style>
:host { display: block; }
</style>
<canvas width="${this.#width}px" height="${this.#height}px"></canvas>
`;
}
cacheDom() {
this.dom = {};
this.dom.canvas = this.shadow.querySelector("canvas");
}
attachEvents() {
}
async bootGpu() {
this.context = this.dom.canvas.getContext("webgl");
this.program = this.context.createProgram();
const vertexShader = compileShader(this.context, `
uniform mat4 uProjectionMatrix;
uniform mat4 uModelMatrix;
attribute vec3 aVertexPosition;
attribute vec3 aVertexColor;
varying mediump vec4 vColor;
void main(){
gl_Position = uProjectionMatrix * uModelMatrix * vec4(aVertexPosition, 1.0);
vColor = vec4(aVertexColor, 1.0);
}
`, this.context.VERTEX_SHADER);
const fragmentShader = compileShader(this.context, `
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
}
`, this.context.FRAGMENT_SHADER);
this.program = compileProgram(this.context, vertexShader, fragmentShader)
this.context.enable(this.context.CULL_FACE);
this.context.cullFace(this.context.BACK);
this.context.useProgram(this.program);
}
createMeshes(){
const tcube = new Mesh(cube);
tcube.setRotation({ x: Math.PI / 4, y: Math.PI / 4 });
tcube.setTranslation({ z: 2, x: 0.75 });
const tpyramid = new Mesh(quadPyramid);
tpyramid.setTranslation({ z: 2, x: -0.75 });
this.meshes = {
pyramid: tpyramid,
cube: tcube
};
}
bindMesh(mesh){
this.bindPositions(mesh.positions);
this.bindColors(mesh.colors);
this.bindIndices(mesh.triangles);
this.bindUniforms(mesh.getModelMatrix());
}
bindPositions(positions) {
const positionBuffer = this.context.createBuffer();
this.context.bindBuffer(this.context.ARRAY_BUFFER, positionBuffer);
this.context.bufferData(this.context.ARRAY_BUFFER, positions, this.context.STATIC_DRAW);
const positionLocation = this.context.getAttribLocation(this.program, "aVertexPosition");
this.context.enableVertexAttribArray(positionLocation);
this.context.vertexAttribPointer(positionLocation, 3, this.context.FLOAT, false, 0, 0);
}
bindColors(colors){
const colorBuffer = this.context.createBuffer();
this.context.bindBuffer(this.context.ARRAY_BUFFER, colorBuffer);
this.context.bufferData(this.context.ARRAY_BUFFER, colors, this.context.STATIC_DRAW);
const vertexColorLocation = this.context.getAttribLocation(this.program, "aVertexColor");
this.context.enableVertexAttribArray(vertexColorLocation);
this.context.vertexAttribPointer(vertexColorLocation, 3, this.context.FLOAT, false, 0, 0);
}
bindIndices(indices) {
const indexBuffer = this.context.createBuffer();
this.context.bindBuffer(this.context.ELEMENT_ARRAY_BUFFER, indexBuffer);
this.context.bufferData(this.context.ELEMENT_ARRAY_BUFFER, indices, this.context.STATIC_DRAW);
}
bindUniforms(modelMatrix){
const modelMatrixLocation = this.context.getUniformLocation(this.program, "uModelMatrix");
this.context.uniformMatrix4fv(modelMatrixLocation, false, modelMatrix);
}
setupGlobalUniforms(){
const projectionMatrix = new Float32Array(getProjectionMatrix(this.#height, this.#width, 90, 0.01, 100).flat());
const projectionLocation = this.context.getUniformLocation(this.program, "uProjectionMatrix");
this.context.uniformMatrix4fv(projectionLocation, false, projectionMatrix);
}
createTexture(image) {
const texture = this.context.createTexture();
this.context.bindTexture(this.context.TEXTURE_2D, texture);
this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_WRAP_S, this.context.CLAMP_TO_EDGE);
this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_WRAP_T, this.context.CLAMP_TO_EDGE);
this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_MIN_FILTER, this.context.NEAREST);
this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_MAG_FILTER, this.context.NEAREST);
this.context.texImage2D(this.context.TEXTURE_2D, 24, this.context.RGBA, this.context.RGBA, this.context.UNSIGNED_BYTE, image);
}
render() {
this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT);
for (const mesh of Object.values(this.meshes)){
this.bindMesh(mesh);
this.context.drawElements(this.context.TRIANGLES, mesh.triangles.length, this.context.UNSIGNED_SHORT, 0);
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (newValue !== oldValue) {
this[name] = newValue;
}
}
set height(value) {
this.#height = value;
if (this.dom) {
this.dom.canvas.height = value;
}
}
set width(value) {
this.#width = value;
if (this.dom) {
this.dom.canvas.height = value;
}
}
set image(value) {
this.#image = value;
loadImage(value)
.then(img => this.createTexture(img));
}
//TODO: throw away program on detach
}
customElements.define("wc-geo-gl", WcGeoGl);
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.