UNPKG

matrix-engine-wgpu

Version:

obj sequence anim +HOTFIX raycast, webGPU powered pwa application. Crazy fast rendering with AmmoJS physics support. Simple raycaster hit object added.

692 lines (620 loc) 24.3 kB
import {mat4, vec3} from 'wgpu-matrix'; import {Position, Rotation} from "./matrix-class"; import {createInputHandler} from "./engine"; import {vertexShadowWGSL} from '../shaders/vertexShadow.wgsl'; import {fragmentWGSL} from '../shaders/fragment.wgsl'; import {vertexWGSL} from '../shaders/vertex.wgsl'; import {degToRad, genName, LOG_FUNNY_SMALL} from './utils'; import Materials from './materials'; import {fragmentVideoWGSL} from '../shaders/fragment.video.wgsl'; export default class MEMeshObj extends Materials { constructor(canvas, device, context, o) { super(device); if(typeof o.name === 'undefined') o.name = genName(9); if(typeof o.raycast === 'undefined') { this.raycast = { enabled: false, radius: 2 }; } else { this.raycast = o.raycast; } this.name = o.name; this.done = false; this.device = device; this.context = context; this.entityArgPass = o.entityArgPass; // comes from engine not from args this.clearColor = "red"; this.video = null; // Mesh stuff - for single mesh or t-posed (fiktive-first in loading order) this.mesh = o.mesh; this.mesh.uvs = this.mesh.textures; console.log(`%c Mesh loaded: ${o.name}`, LOG_FUNNY_SMALL); // ObjSequence animation if(typeof o.objAnim !== 'undefined' && o.objAnim != null) { this.objAnim = o.objAnim; for(var key in this.objAnim.animations) { if(key != 'active') this.objAnim.animations[key].speedCounter = 0; } console.log(`%c Mesh objAnim exist: ${o.objAnim}`, LOG_FUNNY_SMALL); this.drawElements = this.drawElementsAnim; } this.inputHandler = createInputHandler(window, canvas); this.cameras = o.cameras; this.mainCameraParams = { type: o.mainCameraParams.type, responseCoef: o.mainCameraParams.responseCoef } // touchCoordinate.enabled = true; this.lastFrameMS = 0; this.texturesPaths = []; o.texturesPaths.forEach((t) => {this.texturesPaths.push(t)}) this.presentationFormat = navigator.gpu.getPreferredCanvasFormat(); this.position = new Position(o.position.x, o.position.y, o.position.z); this.rotation = new Rotation(o.rotation.x, o.rotation.y, o.rotation.z); this.rotation.rotationSpeed.x = o.rotationSpeed.x; this.rotation.rotationSpeed.y = o.rotationSpeed.y; this.rotation.rotationSpeed.z = o.rotationSpeed.z; this.scale = o.scale; this.runProgram = () => { return new Promise(async (resolve) => { this.shadowDepthTextureSize = 1024; const aspect = canvas.width / canvas.height; this.projectionMatrix = mat4.perspective((2 * Math.PI) / 5, aspect, 1, 2000.0); this.modelViewProjectionMatrix = mat4.create(); // console.log('cube added texturesPaths: ', this.texturesPaths) this.loadTex0(this.texturesPaths).then(() => { // console.log('loaded tex buffer for mesh:', this.texture0) resolve() }) }) } this.runProgram().then(() => { const aspect = canvas.width / canvas.height; // const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); this.context.configure({ device: this.device, format: this.presentationFormat, alphaMode: 'premultiplied', }); // Create the model vertex buffer. this.vertexBuffer = this.device.createBuffer({ size: this.mesh.vertices.length * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.VERTEX, mappedAtCreation: true, }); { // const mapping = new Float32Array(this.vertexBuffer.getMappedRange()); // // for(let i = 0;i < this.mesh.vertices.length;++i) { // // mapping.set(this.mesh.vertices[i], 6 * i); // // mapping.set(this.mesh.normals[i], 6 * i + 3); // // } // this.vertexBuffer.unmap(); new Float32Array(this.vertexBuffer.getMappedRange()).set(this.mesh.vertices); this.vertexBuffer.unmap(); } // NIDZA TEST SECOUND BUFFER // Create the model vertex buffer. this.vertexNormalsBuffer = this.device.createBuffer({ size: this.mesh.vertexNormals.length * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.VERTEX, mappedAtCreation: true, }); { new Float32Array(this.vertexNormalsBuffer.getMappedRange()).set(this.mesh.vertexNormals); this.vertexNormalsBuffer.unmap(); } this.vertexTexCoordsBuffer = this.device.createBuffer({ size: this.mesh.textures.length * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.VERTEX, mappedAtCreation: true, }); { new Float32Array(this.vertexTexCoordsBuffer.getMappedRange()).set(this.mesh.textures); this.vertexTexCoordsBuffer.unmap(); } // Create the model index buffer. this.indexCount = this.mesh.indices.length; const indexCount = this.mesh.indices.length; const size = Math.ceil(indexCount * Uint16Array.BYTES_PER_ELEMENT / 4) * 4; this.indexBuffer = this.device.createBuffer({ size, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST, mappedAtCreation: true }); new Uint16Array(this.indexBuffer.getMappedRange()).set(this.mesh.indices); this.indexBuffer.unmap(); this.indexCount = indexCount; // Create the depth texture for rendering/sampling the shadow map. this.shadowDepthTexture = this.device.createTexture({ size: [this.shadowDepthTextureSize, this.shadowDepthTextureSize, 1], usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, format: 'depth32float', }); this.shadowDepthTextureView = this.shadowDepthTexture.createView(); // Create some common descriptors used for both the shadow pipeline // and the color rendering pipeline. this.vertexBuffers = [ { arrayStride: Float32Array.BYTES_PER_ELEMENT * 3, attributes: [ { // position shaderLocation: 0, offset: 0, format: "float32x3", } ], }, { arrayStride: Float32Array.BYTES_PER_ELEMENT * 3, attributes: [ { // normal shaderLocation: 1, offset: 0, format: "float32x3", }, ], }, { arrayStride: Float32Array.BYTES_PER_ELEMENT * 2, attributes: [ { // uvs shaderLocation: 2, offset: 0, format: "float32x2", }, ], }, ]; this.primitive = { topology: 'triangle-list', // cullMode: 'back', // ORI cullMode: 'none', // ORI }; this.uniformBufferBindGroupLayout = this.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform', }, }, ], }); this.shadowPipeline = this.device.createRenderPipeline({ layout: this.device.createPipelineLayout({ bindGroupLayouts: [ this.uniformBufferBindGroupLayout, this.uniformBufferBindGroupLayout, ], }), vertex: { module: this.device.createShaderModule({ code: vertexShadowWGSL, }), buffers: this.vertexBuffers, }, depthStencil: { depthWriteEnabled: true, depthCompare: 'less', format: 'depth32float', }, primitive: this.primitive, }); // Create a bind group layout which holds the scene uniforms and // the texture+sampler for depth. We create it manually because the WebPU // implementation doesn't infer this from the shader (yet). this.createLayoutForRender() this.setupPipeline(); // this.pipeline = this.device.createRenderPipeline({ // layout: this.device.createPipelineLayout({ // bindGroupLayouts: [this.bglForRender, this.uniformBufferBindGroupLayout], // }), // vertex: { // module: this.device.createShaderModule({ // code: vertexWGSL, // }), // buffers: this.vertexBuffers, // }, // fragment: { // module: this.device.createShaderModule({ // code: fragmentWGSL, // }), // targets: [ // { // format: presentationFormat, // }, // ], // constants: { // shadowDepthTextureSize: this.shadowDepthTextureSize, // }, // }, // depthStencil: { // depthWriteEnabled: true, // depthCompare: 'less', // format: 'depth24plus-stencil8', // }, // primitive, // }); const depthTexture = this.device.createTexture({ size: [canvas.width, canvas.height], format: 'depth24plus-stencil8', usage: GPUTextureUsage.RENDER_ATTACHMENT, }); this.renderPassDescriptor = { colorAttachments: [ { // view is acquired and set in render loop. view: undefined, clearValue: this.clearColor, loadOp: 'clear', // load old fix for FF storeOp: 'store', }, ], depthStencilAttachment: { view: depthTexture.createView(), depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store', stencilClearValue: 0, stencilLoadOp: 'clear', stencilStoreOp: 'store', }, }; this.modelUniformBuffer = this.device.createBuffer({ size: 4 * 16, // 4x4 matrix usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); this.sceneUniformBuffer = this.device.createBuffer({ // Two 4x4 viewProj matrices, // one for the camera and one for the light. // Then a vec3 for the light position. // Rounded to the nearest multiple of 16. size: 2 * 4 * 16 + 4 * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); this.sceneBindGroupForShadow = this.device.createBindGroup({ layout: this.uniformBufferBindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.sceneUniformBuffer, }, }, ], }); // -------------------------- this.createBindGroupForRender(); this.modelBindGroup = this.device.createBindGroup({ layout: this.uniformBufferBindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.modelUniformBuffer, }, }, ], }); // Rotates the camera around the origin based on time. this.getTransformationMatrix = (pos) => { const now = Date.now(); const deltaTime = (now - this.lastFrameMS) / this.mainCameraParams.responseCoef; this.lastFrameMS = now; // const this.viewMatrix = mat4.identity() const camera = this.cameras[this.mainCameraParams.type]; this.viewMatrix = camera.update(deltaTime, this.inputHandler()); const scaleVec = [1, 1, 1]; // your desired scale OPTION 1 const scaleMatrix = mat4.scaling(scaleVec); // Apply scaling mat4.multiply(scaleMatrix, this.viewMatrix, this.viewMatrix); mat4.translate(this.viewMatrix, vec3.fromValues(pos.x, pos.y, pos.z), this.viewMatrix); if(this.itIsPhysicsBody == true) { mat4.rotate( this.viewMatrix, vec3.fromValues(this.rotation.axis.x, this.rotation.axis.y, this.rotation.axis.z), degToRad(this.rotation.angle), this.viewMatrix) // console.info('angle: ', this.rotation.angle, ' axis ', this.rotation.axis.x, ' , ', this.rotation.axis.y, ' , ', this.rotation.axis.z) } else { mat4.rotateX(this.viewMatrix, Math.PI * this.rotation.getRotX(), this.viewMatrix); mat4.rotateY(this.viewMatrix, Math.PI * this.rotation.getRotY(), this.viewMatrix); mat4.rotateZ(this.viewMatrix, Math.PI * this.rotation.getRotZ(), this.viewMatrix); // console.info('NOT PHYSICS angle: ', this.rotation.angle, ' axis ', this.rotation.axis.x, ' , ', this.rotation.axis.y, ' , ', this.rotation.axis.z) } mat4.multiply(this.projectionMatrix, this.viewMatrix, this.modelViewProjectionMatrix); return this.modelViewProjectionMatrix; } this.getModelMatrix = (pos) => { let modelMatrix = mat4.identity(); mat4.translate(modelMatrix, [pos.x, pos.y, pos.z], modelMatrix); if(this.itIsPhysicsBody) { mat4.rotate(modelMatrix, [this.rotation.axis.x, this.rotation.axis.y, this.rotation.axis.z], degToRad(this.rotation.angle), modelMatrix ); } else { mat4.rotateX(modelMatrix, this.rotation.getRotX(), modelMatrix); mat4.rotateY(modelMatrix, this.rotation.getRotY(), modelMatrix); mat4.rotateZ(modelMatrix, this.rotation.getRotZ(), modelMatrix); } // Apply scale if you have it, e.g.: // mat4.scale(modelMatrix, modelMatrix, [this.scale.x, this.scale.y, this.scale.z]); return modelMatrix; }; this.upVector = vec3.fromValues(0, 1, 0); this.origin = vec3.fromValues(0, 0, 0); this.lightPosition = vec3.fromValues(0, 0, 0); this.lightViewMatrix = mat4.lookAt(this.lightPosition, this.origin, this.upVector); const lightProjectionMatrix = mat4.create(); var myLMargin = 100; { const left = -myLMargin; const right = myLMargin; const bottom = -myLMargin; const top = myLMargin; const near = -200; const far = 300; mat4.ortho(left, right, bottom, top, near, far, lightProjectionMatrix); // test // mat4.ortho(right, left, top, bottom, near, far, lightProjectionMatrix); } this.lightViewProjMatrix = mat4.multiply( lightProjectionMatrix, this.lightViewMatrix ); // looks like affect on transformations for now const 0 const modelMatrix = mat4.translation([0, 0, 0]); // The camera/light aren't moving, so write them into buffers now. { const lightMatrixData = this.lightViewProjMatrix; // as Float32Array; this.device.queue.writeBuffer( this.sceneUniformBuffer, 0, lightMatrixData.buffer, lightMatrixData.byteOffset, lightMatrixData.byteLength ); const lightData = this.lightPosition; this.device.queue.writeBuffer( this.sceneUniformBuffer, 128, lightData.buffer, lightData.byteOffset, lightData.byteLength ); const modelData = modelMatrix; this.device.queue.writeBuffer( this.modelUniformBuffer, 0, modelData.buffer, modelData.byteOffset, modelData.byteLength ); } this.shadowPassDescriptor = { colorAttachments: [], depthStencilAttachment: { view: this.shadowDepthTextureView, depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store', }, }; this.done = true; }).then(() => { if(typeof this.objAnim !== 'undefined' && this.objAnim !== null) { console.log('after all load configutr mesh list buffers') this.updateMeshListBuffers() } }) } updateLightsTest = (position) => { console.log('Update light position.', position) this.lightPosition = vec3.fromValues(position[0], position[1], position[2]); this.lightViewMatrix = mat4.lookAt(this.lightPosition, this.origin, this.upVector); const lightProjectionMatrix = mat4.create(); { const left = -80; const right = 80; const bottom = -80; const top = 80; const near = -200; const far = 300; mat4.ortho(left, right, bottom, top, near, far, lightProjectionMatrix); } this.lightViewProjMatrix = mat4.multiply( lightProjectionMatrix, this.lightViewMatrix ); // looks like affect on transformations for now const 0 const modelMatrix = mat4.translation([0, 0, 0]); // The camera/light aren't moving, so write them into buffers now. { const lightMatrixData = this.lightViewProjMatrix; // as Float32Array; this.device.queue.writeBuffer( this.sceneUniformBuffer, 0, // 0 ori lightMatrixData.buffer, lightMatrixData.byteOffset, lightMatrixData.byteLength ); const lightData = this.lightPosition; this.device.queue.writeBuffer( this.sceneUniformBuffer, 256, lightData.buffer, lightData.byteOffset, lightData.byteLength ); const modelData = modelMatrix; this.device.queue.writeBuffer( this.modelUniformBuffer, 0, modelData.buffer, modelData.byteOffset, modelData.byteLength ); } this.shadowPassDescriptor = { colorAttachments: [], depthStencilAttachment: { view: this.shadowDepthTextureView, depthClearValue: 1.0, // ori 1.0 depthLoadOp: 'clear', depthStoreOp: 'store', }, }; /////////////////////// } setupPipeline = () => { this.pipeline = this.device.createRenderPipeline({ layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.bglForRender, this.uniformBufferBindGroupLayout], }), vertex: { entryPoint: 'main', // ✅ Add this module: this.device.createShaderModule({ code: vertexWGSL, }), buffers: this.vertexBuffers, }, fragment: { entryPoint: 'main', // ✅ Add this module: this.device.createShaderModule({ code: (this.isVideo == true ? fragmentVideoWGSL : fragmentWGSL), }), targets: [ { format: this.presentationFormat, }, ], constants: { shadowDepthTextureSize: this.shadowDepthTextureSize, }, }, depthStencil: { depthWriteEnabled: true, depthCompare: 'less', format: 'depth24plus-stencil8', }, primitive: this.primitive, }); } draw = () => { if(this.done == false) return; const transformationMatrix = this.getTransformationMatrix(this.position); this.device.queue.writeBuffer(this.sceneUniformBuffer, 64, transformationMatrix.buffer, transformationMatrix.byteOffset, transformationMatrix.byteLength); this.renderPassDescriptor.colorAttachments[0].view = this.context .getCurrentTexture() .createView(); } drawElements = (renderPass) => { if(this.isVideo) { this.updateVideoTexture(); } renderPass.setBindGroup(0, this.sceneBindGroupForRender); renderPass.setBindGroup(1, this.modelBindGroup); renderPass.setVertexBuffer(0, this.vertexBuffer); renderPass.setVertexBuffer(1, this.vertexNormalsBuffer); renderPass.setVertexBuffer(2, this.vertexTexCoordsBuffer); renderPass.setIndexBuffer(this.indexBuffer, 'uint16'); renderPass.drawIndexed(this.indexCount); } // test createGPUBuffer(dataArray, usage) { if(!dataArray || typeof dataArray.length !== 'number') { throw new Error('Invalid data array passed to createGPUBuffer'); } const size = dataArray.length * dataArray.BYTES_PER_ELEMENT; if(!Number.isFinite(size) || size <= 0) { throw new Error(`Invalid buffer size: ${size}`); } const buffer = this.device.createBuffer({ size, usage, mappedAtCreation: true, }); const writeArray = dataArray.constructor === Float32Array ? new Float32Array(buffer.getMappedRange()) : new Uint16Array(buffer.getMappedRange()); writeArray.set(dataArray); buffer.unmap(); return buffer; } updateMeshListBuffers() { for(const key in this.objAnim.meshList) { const mesh = this.objAnim.meshList[key]; mesh.vertexBuffer = this.device.createBuffer({ size: mesh.vertices.length * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.VERTEX, mappedAtCreation: true, }); new Float32Array(mesh.vertexBuffer.getMappedRange()).set(mesh.vertices); mesh.vertexBuffer.unmap(); // Normals mesh.vertexNormalsBuffer = this.device.createBuffer({ size: mesh.vertexNormals.length * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.VERTEX, mappedAtCreation: true, }); new Float32Array(mesh.vertexNormalsBuffer.getMappedRange()).set(mesh.vertexNormals); mesh.vertexNormalsBuffer.unmap(); // UVs mesh.vertexTexCoordsBuffer = this.device.createBuffer({ size: mesh.textures.length * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.VERTEX, mappedAtCreation: true, }); new Float32Array(mesh.vertexTexCoordsBuffer.getMappedRange()).set(mesh.textures); mesh.vertexTexCoordsBuffer.unmap(); // Indices const indexCount = mesh.indices.length; const indexSize = Math.ceil(indexCount * Uint16Array.BYTES_PER_ELEMENT / 4) * 4; mesh.indexBuffer = this.device.createBuffer({ size: indexSize, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST, mappedAtCreation: true, }); new Uint16Array(mesh.indexBuffer.getMappedRange()).set(mesh.indices); mesh.indexBuffer.unmap(); mesh.indexCount = indexCount; } } drawElementsAnim = (renderPass) => { renderPass.setBindGroup(0, this.sceneBindGroupForRender); renderPass.setBindGroup(1, this.modelBindGroup); const mesh = this.objAnim.meshList[this.objAnim.id + this.objAnim.currentAni]; renderPass.setVertexBuffer(0, mesh.vertexBuffer); renderPass.setVertexBuffer(1, mesh.vertexNormalsBuffer); renderPass.setVertexBuffer(2, mesh.vertexTexCoordsBuffer); renderPass.setIndexBuffer(mesh.indexBuffer, 'uint16'); renderPass.drawIndexed(mesh.indexCount); if(this.objAnim.playing == true) { if(this.objAnim.animations[this.objAnim.animations.active].speedCounter >= this.objAnim.animations[this.objAnim.animations.active].speed) { this.objAnim.currentAni++; this.objAnim.animations[this.objAnim.animations.active].speedCounter = 0; } else { this.objAnim.animations[this.objAnim.animations.active].speedCounter++; } if(this.objAnim.currentAni >= this.objAnim.animations[this.objAnim.animations.active].to) { this.objAnim.currentAni = this.objAnim.animations[this.objAnim.animations.active].from; } } } drawShadows = (shadowPass) => { shadowPass.setBindGroup(0, this.sceneBindGroupForShadow); shadowPass.setBindGroup(1, this.modelBindGroup); shadowPass.setVertexBuffer(0, this.vertexBuffer); shadowPass.setVertexBuffer(1, this.vertexNormalsBuffer); shadowPass.setVertexBuffer(2, this.vertexTexCoordsBuffer); shadowPass.setIndexBuffer(this.indexBuffer, 'uint16'); shadowPass.drawIndexed(this.indexCount); } }