import untypedStars from './data/stars.json'; import untypedConstellations from './data/constellations.json'; import { Stats } from 'stats.ts'; const shaderCode = ` struct VertexOut { @builtin(position) position: vec4f, @location(0) color: vec4f, @location(1) uv: vec2f } struct Star { pos: vec3f, lum: f32 } @group(0) @binding(0) var _ViewProjectionMatrix: mat4x4f; @group(0) @binding(1) var stars: array; const quad: array = array( vec4f(0, 0, 0, 0), vec4f(0, 1, 0, 0), vec4f(1, 0, 0, 0), vec4f(1, 1, 0, 0), ); @vertex fn vertex_main(@builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32) -> VertexOut { var star: Star = stars[instance_index]; var output: VertexOut; output.position = vec4f(star.pos, 0) + quad[vertex_index]; output.color = vec4(star.lum, star.lum, star.lum, 1); output.uv = vec2f(quad[vertex_index].x, quad[vertex_index].y); return output; } @fragment fn fragment_main(fragData: VertexOut) -> @location(0) vec4f { return fragData.color * distance(fragData.uv, vec2f(0.5, 0.5)); } `; type StarID = number; type Star = { id: StarID; x: number; y: number; z: number; ci: number; lum: number; }; type Constellation = { con: string; con_id: string; lines: StarID[][]; certainty: string | null; description: string | null; semantics: string[] | null; }; class Matrix4x4 { private _buffer: ArrayBuffer; private _view: Float32Array; constructor() { this._view = new Float32Array(16); this._buffer = this._view.buffer as ArrayBuffer; } static translation(x: number, y: number, z: number): Matrix4x4 { const mat = new Matrix4x4(); mat.view[ 0] = 1; mat.view[ 1] = 0; mat.view[ 2] = 0; mat.view[ 3] = 0; mat.view[ 4] = 0; mat.view[ 5] = 1; mat.view[ 6] = 0; mat.view[ 7] = 0; mat.view[ 8] = 0; mat.view[ 9] = 0; mat.view[10] = 1; mat.view[11] = 0; mat.view[12] = x; mat.view[13] = y; mat.view[14] = z; mat.view[15] = 1; return mat; } static rotationX(angle: number): Matrix4x4 { const c = Math.cos(angle); const s = Math.sin(angle); const mat = new Matrix4x4(); mat.view[ 0] = 1; mat.view[ 1] = 0; mat.view[ 2] = 0; mat.view[ 3] = 0; mat.view[ 4] = 0; mat.view[ 5] = c; mat.view[ 6] = s; mat.view[ 7] = 0; mat.view[ 8] = 0; mat.view[ 9] = -s; mat.view[10] = c; mat.view[11] = 0; mat.view[12] = 0; mat.view[13] = 0; mat.view[14] = 0; mat.view[15] = 1; return mat; } static rotationY(angle: number): Matrix4x4 { const c = Math.cos(angle); const s = Math.sin(angle); const mat = new Matrix4x4(); mat.view[ 0] = c; mat.view[ 1] = 0; mat.view[ 2] = -s; mat.view[ 3] = 0; mat.view[ 4] = 0; mat.view[ 5] = 1; mat.view[ 6] = 0; mat.view[ 7] = 0; mat.view[ 8] = s; mat.view[ 9] = 0; mat.view[10] = c; mat.view[11] = 0; mat.view[12] = 0; mat.view[13] = 0; mat.view[14] = 0; mat.view[15] = 1; return mat; } static rotationZ(angle: number): Matrix4x4 { const c = Math.cos(angle); const s = Math.sin(angle); const mat = new Matrix4x4(); mat.view[ 0] = c; mat.view[ 1] = s; mat.view[ 2] = 0; mat.view[ 3] = 0; mat.view[ 4] = -s; mat.view[ 5] = c; mat.view[ 6] = 0; mat.view[ 7] = 0; mat.view[ 8] = 0; mat.view[ 9] = 0; mat.view[10] = 1; mat.view[11] = 0; mat.view[12] = 0; mat.view[13] = 0; mat.view[14] = 0; mat.view[15] = 1; return mat; } static scaling(sx: number, sy: number, sz: number): Matrix4x4 { const mat = new Matrix4x4(); mat.view[ 0] = sx; mat.view[ 1] = 0; mat.view[ 2] = 0; mat.view[ 3] = 0; mat.view[ 4] = 0; mat.view[ 5] = sy; mat.view[ 6] = 0; mat.view[ 7] = 0; mat.view[ 8] = 0; mat.view[ 9] = 0; mat.view[10] = sz; mat.view[11] = 0; mat.view[12] = 0; mat.view[13] = 0; mat.view[14] = 0; mat.view[15] = 1; return mat; } translate(x: number, y: number, z: number): this { return this.multiply(this, Matrix4x4.translation(x, y, z)); } rotateX(angle: number): this { return this.multiply(this, Matrix4x4.rotationX(angle)); } rotateY(angle: number): this { return this.multiply(this, Matrix4x4.rotationY(angle)); } rotateZ(angle: number): this { return this.multiply(this, Matrix4x4.rotationZ(angle)); } scale(sx: number, sy: number, sz: number): this { return this.multiply(this, Matrix4x4.scaling(sx, sy, sz)); } multiply(a: Matrix4x4, b: Matrix4x4): this { const b00 = b.view[0 * 4 + 0]!; const b01 = b.view[0 * 4 + 1]!; const b02 = b.view[0 * 4 + 2]!; const b03 = b.view[0 * 4 + 3]!; const b10 = b.view[1 * 4 + 0]!; const b11 = b.view[1 * 4 + 1]!; const b12 = b.view[1 * 4 + 2]!; const b13 = b.view[1 * 4 + 3]!; const b20 = b.view[2 * 4 + 0]!; const b21 = b.view[2 * 4 + 1]!; const b22 = b.view[2 * 4 + 2]!; const b23 = b.view[2 * 4 + 3]!; const b30 = b.view[3 * 4 + 0]!; const b31 = b.view[3 * 4 + 1]!; const b32 = b.view[3 * 4 + 2]!; const b33 = b.view[3 * 4 + 3]!; const a00 = a.view[0 * 4 + 0]!; const a01 = a.view[0 * 4 + 1]!; const a02 = a.view[0 * 4 + 2]!; const a03 = a.view[0 * 4 + 3]!; const a10 = a.view[1 * 4 + 0]!; const a11 = a.view[1 * 4 + 1]!; const a12 = a.view[1 * 4 + 2]!; const a13 = a.view[1 * 4 + 3]!; const a20 = a.view[2 * 4 + 0]!; const a21 = a.view[2 * 4 + 1]!; const a22 = a.view[2 * 4 + 2]!; const a23 = a.view[2 * 4 + 3]!; const a30 = a.view[3 * 4 + 0]!; const a31 = a.view[3 * 4 + 1]!; const a32 = a.view[3 * 4 + 2]!; const a33 = a.view[3 * 4 + 3]!; this._view[0] = b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30; this._view[1] = b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31; this._view[2] = b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32; this._view[3] = b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33; this._view[4] = b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30; this._view[5] = b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31; this._view[6] = b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32; this._view[7] = b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33; this._view[8] = b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30; this._view[9] = b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31; this._view[10] = b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32; this._view[11] = b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33; this._view[12] = b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30; this._view[13] = b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31; this._view[14] = b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32; this._view[15] = b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33; return this; } inverse() { const m00 = this._view[0 * 4 + 0]!; const m01 = this._view[0 * 4 + 1]!; const m02 = this._view[0 * 4 + 2]!; const m03 = this._view[0 * 4 + 3]!; const m10 = this._view[1 * 4 + 0]!; const m11 = this._view[1 * 4 + 1]!; const m12 = this._view[1 * 4 + 2]!; const m13 = this._view[1 * 4 + 3]!; const m20 = this._view[2 * 4 + 0]!; const m21 = this._view[2 * 4 + 1]!; const m22 = this._view[2 * 4 + 2]!; const m23 = this._view[2 * 4 + 3]!; const m30 = this._view[3 * 4 + 0]!; const m31 = this._view[3 * 4 + 1]!; const m32 = this._view[3 * 4 + 2]!; const m33 = this._view[3 * 4 + 3]!; const tmp0 = m22 * m33; const tmp1 = m32 * m23; const tmp2 = m12 * m33; const tmp3 = m32 * m13; const tmp4 = m12 * m23; const tmp5 = m22 * m13; const tmp6 = m02 * m33; const tmp7 = m32 * m03; const tmp8 = m02 * m23; const tmp9 = m22 * m03; const tmp10 = m02 * m13; const tmp11 = m12 * m03; const tmp12 = m20 * m31; const tmp13 = m30 * m21; const tmp14 = m10 * m31; const tmp15 = m30 * m11; const tmp16 = m10 * m21; const tmp17 = m20 * m11; const tmp18 = m00 * m31; const tmp19 = m30 * m01; const tmp20 = m00 * m21; const tmp21 = m20 * m01; const tmp22 = m00 * m11; const tmp23 = m10 * m01; const t0 = (tmp0 * m11 + tmp3 * m21 + tmp4 * m31) - (tmp1 * m11 + tmp2 * m21 + tmp5 * m31); const t1 = (tmp1 * m01 + tmp6 * m21 + tmp9 * m31) - (tmp0 * m01 + tmp7 * m21 + tmp8 * m31); const t2 = (tmp2 * m01 + tmp7 * m11 + tmp10 * m31) - (tmp3 * m01 + tmp6 * m11 + tmp11 * m31); const t3 = (tmp5 * m01 + tmp8 * m11 + tmp11 * m21) - (tmp4 * m01 + tmp9 * m11 + tmp10 * m21); const d = 1 / (m00 * t0 + m10 * t1 + m20 * t2 + m30 * t3); this._view[0] = d * t0; this._view[1] = d * t1; this._view[2] = d * t2; this._view[3] = d * t3; this._view[4] = d * ((tmp1 * m10 + tmp2 * m20 + tmp5 * m30) - (tmp0 * m10 + tmp3 * m20 + tmp4 * m30)); this._view[5] = d * ((tmp0 * m00 + tmp7 * m20 + tmp8 * m30) - (tmp1 * m00 + tmp6 * m20 + tmp9 * m30)); this._view[6] = d * ((tmp3 * m00 + tmp6 * m10 + tmp11 * m30) - (tmp2 * m00 + tmp7 * m10 + tmp10 * m30)); this._view[7] = d * ((tmp4 * m00 + tmp9 * m10 + tmp10 * m20) - (tmp5 * m00 + tmp8 * m10 + tmp11 * m20)); this._view[8] = d * ((tmp12 * m13 + tmp15 * m23 + tmp16 * m33) - (tmp13 * m13 + tmp14 * m23 + tmp17 * m33)); this._view[9] = d * ((tmp13 * m03 + tmp18 * m23 + tmp21 * m33) - (tmp12 * m03 + tmp19 * m23 + tmp20 * m33)); this._view[10] = d * ((tmp14 * m03 + tmp19 * m13 + tmp22 * m33) - (tmp15 * m03 + tmp18 * m13 + tmp23 * m33)); this._view[11] = d * ((tmp17 * m03 + tmp20 * m13 + tmp23 * m23) - (tmp16 * m03 + tmp21 * m13 + tmp22 * m23)); this._view[12] = d * ((tmp14 * m22 + tmp17 * m32 + tmp13 * m12) - (tmp16 * m32 + tmp12 * m12 + tmp15 * m22)); this._view[13] = d * ((tmp20 * m32 + tmp12 * m02 + tmp19 * m22) - (tmp18 * m22 + tmp21 * m32 + tmp13 * m02)); this._view[14] = d * ((tmp18 * m12 + tmp23 * m32 + tmp15 * m02) - (tmp22 * m32 + tmp14 * m02 + tmp19 * m12)); this._view[15] = d * ((tmp22 * m22 + tmp16 * m02 + tmp21 * m12) - (tmp20 * m12 + tmp23 * m22 + tmp17 * m02)); return this; } camera(transform: { position: { x: number; y: number; z: number; }, yaw: number, pitch: number }): this { const cosPitch = Math.cos(transform.pitch); const sinPitch = Math.sin(transform.pitch); const cosYaw = Math.cos(transform.yaw); const sinYaw = Math.sin(transform.yaw); this._view[0] = cosYaw; this._view[1] = 0; this._view[2] = -sinYaw; this._view[3] = 0; this._view[4] = sinYaw * sinPitch; this._view[5] = cosPitch; this._view[6] = cosYaw * sinPitch; this._view[7] = 0; this._view[8] = sinYaw * cosPitch; this._view[9] = -sinPitch; this._view[10] = cosYaw * cosPitch; this._view[11] = 0; this._view[12] = transform.position.x; this._view[13] = transform.position.y; this._view[14] = transform.position.z; this._view[15] = 1; return this; } perspective(aspect: number, fovY: number, near: number, far: number): this { const f = Math.tan(Math.PI * 0.5 - 0.5 * fovY); const rangeInv = 1 / (near - far); this._view[0] = f / aspect; this._view[1] = 0; this._view[2] = 0; this._view[3] = 0; this._view[4] = 0; this._view[5] = f; this._view[6] = 0; this._view[7] = 0; this._view[8] = 0; this._view[9] = 0; this._view[10] = far * rangeInv; this._view[11] = -1; this._view[12] = 0; this._view[13] = 0; this._view[14] = near * far * rangeInv; this._view[15] = 0; return this; } copy(buffer: ArrayBuffer, offset?: number) { new Float32Array(buffer).set(this._view, offset); } get view() { return this._view; } } //@ts-ignore const stars: Star[] = untypedStars; const constellations: Constellation[] = untypedConstellations; let canvas: HTMLCanvasElement, device: GPUDevice | undefined, pipeline: GPURenderPipeline, context: GPUCanvasContext | null, bindGroup: GPUBindGroup, viewport: DOMRect; let camera = { projection: new Matrix4x4(), view: new Matrix4x4(), transform: { position: { x: 0, y: 0, z: 0 }, yaw: 0, pitch: 0 }, final: new Matrix4x4(), _viewDirty: true, _projectionDirty: true }; let stats: Stats; function fillBuffer(buffer: ArrayBuffer) { const array = new Float32Array(buffer); for(let i = 0; i < stars.length; i++) { array[i * 5 + 0] = stars[i]!.x; array[i * 5 + 1] = stars[i]!.y; array[i * 5 + 2] = stars[i]!.z; array[i * 5 + 3] = stars[i]!.lum; //array[i * 5 + 4] = stars[i]!.ci; } } function resize() { viewport = document.body.getBoundingClientRect(); canvas!.width = viewport.width; canvas!.height = viewport.height; camera.projection.perspective(viewport.width / viewport.height, 60, 0.1, 2000); } function computeViewProjectionMatrix() { let _dirty = false; if(camera._viewDirty) { _dirty = true; camera.view.camera(camera.transform).inverse(); camera._viewDirty = false; } if(camera._projectionDirty) { _dirty = true; camera.projection.perspective(viewport.width / viewport.height, 60, 0.1, 2000); camera._projectionDirty = false; } if(_dirty) { camera.final.multiply(camera.projection, camera.view); } } async function init() { canvas = document.createElement("canvas"); context = canvas.getContext('webgpu'); document.body.appendChild(canvas); resize(); document.addEventListener('resize', resize); stats = new Stats(); document.body.appendChild(stats.dom); const adapter = await navigator.gpu.requestAdapter({ }); device = await adapter?.requestDevice({ label: 'Device' }); if(device && context) { context.configure({ device, format: navigator.gpu.getPreferredCanvasFormat(), alphaMode: 'premultiplied', }); const shader = device.createShaderModule({ code: shaderCode, label: 'default-shader' }); const buffer = device.createBuffer({ size: stars.length * 4 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, mappedAtCreation: true, label: 'instanced_buffer' }); const arrayBuffer = buffer.getMappedRange(); fillBuffer(arrayBuffer); buffer.unmap(); device.queue.writeBuffer(buffer, 0, arrayBuffer); const uniformBuffer = device.createBuffer({ size: 16 * 4 /* matrix4x4 */, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, mappedAtCreation: true, label: 'uniform_buffer' }); const uniformArrayBuffer = uniformBuffer.getMappedRange(); computeViewProjectionMatrix(); camera.final.copy(uniformArrayBuffer); uniformBuffer.unmap(); device.queue.writeBuffer(uniformBuffer, 0, uniformArrayBuffer); const bindGroupLayout = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform', } }, { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage', } }] }); bindGroup = device.createBindGroup({ layout: bindGroupLayout, entries: [{ binding: 0, resource: { buffer: uniformBuffer, } }, { binding: 1, resource: { buffer: buffer, } }] }); pipeline = device.createRenderPipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [ bindGroupLayout ], }), vertex: { module: shader, entryPoint: 'vertex_main' }, fragment: { targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }], module: shader, entryPoint: 'fragment_main' }, primitive: { topology: 'triangle-list', cullMode: 'front', } }); requestAnimationFrame(render); } else { document.body.append('Erreur de chargement (aucun GPU disponible)'); } } function render(time: DOMHighResTimeStamp) { stats.begin(); const commandEncoder = device!.createCommandEncoder({ label: 'command-encoder' }); const pass = commandEncoder.beginRenderPass({ label: 'render-pass', colorAttachments: [{ loadOp: "clear", storeOp: "store", clearValue: { r: 0, g: 0, b: 0, a: 1 }, view: context!.getCurrentTexture().createView({ label: 'render-target', usage: GPUTextureUsage.RENDER_ATTACHMENT }), }], }); pass.setPipeline(pipeline!); pass.setBindGroup(0, bindGroup); pass.setViewport(viewport.x, viewport.y, viewport.width, viewport.height, 0, 1); pass.draw(4, stars.length, 0, 0); pass.end(); device!.queue.submit([ commandEncoder.finish() ]); stats.end(); requestAnimationFrame(render); } document.addEventListener('DOMContentLoaded', init);