From 38f18edb42be5ccfd64ed71155c43f1ad0416594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Wed, 2 Jul 2025 02:10:06 +0200 Subject: [PATCH 1/4] Add ACES tonemapping --- ogl-engine/engine/Scene.ts | 9 +++++++++ ogl-engine/engine/engine3d.ts | 2 ++ ogl-engine/post/tonemapping.frag.glsl | 6 ++++++ ogl-engine/shader/aces.glsl | 11 +++++++++++ 4 files changed, 28 insertions(+) create mode 100644 ogl-engine/shader/aces.glsl diff --git a/ogl-engine/engine/Scene.ts b/ogl-engine/engine/Scene.ts index 15ba3f2e..ad174383 100644 --- a/ogl-engine/engine/Scene.ts +++ b/ogl-engine/engine/Scene.ts @@ -169,6 +169,15 @@ export class TacticalMapScene extends Scene { max: 2, } ); + this.pane.addBinding( + this.engine.uniforms.env.postProcessing.tonemapping.uAces, + "value", + { + label: "ACES Enabled", + min: 0, + max: 1, + } + ); } destroy() { diff --git a/ogl-engine/engine/engine3d.ts b/ogl-engine/engine/engine3d.ts index ce145d4e..931a5113 100644 --- a/ogl-engine/engine/engine3d.ts +++ b/ogl-engine/engine/engine3d.ts @@ -73,6 +73,7 @@ export class Engine3D extends Engine { uGamma: { value: number }; uSaturation: { value: number }; uContrast: { value: number }; + uAces: { value: number }; }; }; }; @@ -154,6 +155,7 @@ export class Engine3D extends Engine { uGamma: { value: 1.08 }, uContrast: { value: 1 }, uSaturation: { value: 1 }, + uAces: { value: 1 }, }, }, }, diff --git a/ogl-engine/post/tonemapping.frag.glsl b/ogl-engine/post/tonemapping.frag.glsl index 54cb3347..09566958 100644 --- a/ogl-engine/post/tonemapping.frag.glsl +++ b/ogl-engine/post/tonemapping.frag.glsl @@ -1,14 +1,20 @@ precision highp float; +#pragma glslify: aces = require("./ogl-engine/shader/aces"); + uniform sampler2D tMap; uniform float uGamma; uniform float uSaturation; uniform float uContrast; +uniform float uAces; varying vec2 vUv; void main() { vec4 color = texture2D(tMap, vUv); + if(uAces > 0.5) { + color.rgb = aces(color.rgb); + } color = vec4(color.rgb, 1.); color = vec4(pow(color.rgb, vec3(1.0 / uGamma)), color.a); diff --git a/ogl-engine/shader/aces.glsl b/ogl-engine/shader/aces.glsl new file mode 100644 index 00000000..ca2a4b99 --- /dev/null +++ b/ogl-engine/shader/aces.glsl @@ -0,0 +1,11 @@ +const float a = 2.51; +const float b = 0.03; +const float c = 2.43; +const float d = 0.59; +const float e = 0.14; + +vec3 aces(vec3 x) { + return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0); +} + +#pragma glslify: export(aces) From 76bdf840d922b188e7070450af46f5465ca79c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Thu, 3 Jul 2025 23:41:14 +0200 Subject: [PATCH 2/4] Use RGBA16F type texture --- core/world/data/map.json | 4 +- ogl-engine/builders/Planet.ts | 2 +- ogl-engine/engine/Scene.ts | 13 +++++- ogl-engine/engine/engine3d.ts | 14 ++++++- ogl-engine/materials/atmosphere/atmosphere.ts | 4 +- .../materials/atmosphere/shader.frag.glsl | 10 ++--- .../entityIndicator/shader.frag.glsl | 2 +- ogl-engine/materials/star/shader.frag.glsl | 4 +- ogl-engine/post/tonemapping.frag.glsl | 14 ++++--- ogl-engine/post/ui.frag.glsl | 2 +- ogl-engine/shader/aces.glsl | 11 ----- ogl-engine/shader/pbr.glsl | 41 +++++++++---------- package-lock.json | 26 ++++++++++++ package.json | 1 + 14 files changed, 89 insertions(+), 59 deletions(-) delete mode 100644 ogl-engine/shader/aces.glsl diff --git a/core/world/data/map.json b/core/world/data/map.json index 936d80f5..be13197f 100644 --- a/core/world/data/map.json +++ b/core/world/data/map.json @@ -101,7 +101,7 @@ "scale": 1594937, "textureSet": "gaia", "name": "Gaia", - "atmosphere": [-2.01, -8.31] + "atmosphere": [-0.21, -0.48] } ] }, @@ -372,7 +372,7 @@ "scale": 3116645.82, "textureSet": "ansura", "name": "Ansura", - "atmosphere": [-1.99, -8.28] + "atmosphere": [-0.3, -0.75] } ] }, diff --git a/ogl-engine/builders/Planet.ts b/ogl-engine/builders/Planet.ts index d34754f4..f85000b2 100644 --- a/ogl-engine/builders/Planet.ts +++ b/ogl-engine/builders/Planet.ts @@ -40,7 +40,7 @@ export class Planet extends BackgroundProp { }), frustumCulled: false, }); - this.atmosphere.scale.set(2.3); + this.atmosphere.scale.set(2.15); this.atmosphere.setParent(this); this.task = this.engine.addOnBeforeRenderTask(() => { diff --git a/ogl-engine/engine/Scene.ts b/ogl-engine/engine/Scene.ts index ad174383..6c12d109 100644 --- a/ogl-engine/engine/Scene.ts +++ b/ogl-engine/engine/Scene.ts @@ -170,10 +170,19 @@ export class TacticalMapScene extends Scene { } ); this.pane.addBinding( - this.engine.uniforms.env.postProcessing.tonemapping.uAces, + this.engine.uniforms.env.postProcessing.tonemapping.uExposure, "value", { - label: "ACES Enabled", + label: "Exposure", + min: 0.5, + max: 2.5, + } + ); + this.pane.addBinding( + this.engine.uniforms.env.postProcessing.tonemapping.uMap, + "value", + { + label: "Tonemapping Enabled", min: 0, max: 1, } diff --git a/ogl-engine/engine/engine3d.ts b/ogl-engine/engine/engine3d.ts index 931a5113..64781216 100644 --- a/ogl-engine/engine/engine3d.ts +++ b/ogl-engine/engine/engine3d.ts @@ -73,7 +73,8 @@ export class Engine3D extends Engine { uGamma: { value: number }; uSaturation: { value: number }; uContrast: { value: number }; - uAces: { value: number }; + uExposure: { value: number }; + uMap: { value: number }; }; }; }; @@ -107,6 +108,11 @@ export class Engine3D extends Engine { init = (canvas: HTMLCanvasElement) => { super.init(canvas); + this.gl.getExtension("EXT_color_buffer_float"); + this.gl.getExtension("WEBGL_color_buffer_float"); + this.gl.getExtension("OES_texture_half_float"); + this.gl.getExtension("OES_texture_half_float_linear"); + const gl = this.renderer.gl; this.camera = new Camera(this); this.camera.position.set(50, 50, 50); @@ -117,6 +123,9 @@ export class Engine3D extends Engine { this.renderTarget = new RenderTarget(gl, { // Color, bloom and UI color: 3, + type: gl.HALF_FLOAT, + format: gl.RGBA, + internalFormat: gl.RGBA16F, }); this.uniforms.env.tEnvMap.value = new Texture(this.renderer.gl); @@ -155,7 +164,8 @@ export class Engine3D extends Engine { uGamma: { value: 1.08 }, uContrast: { value: 1 }, uSaturation: { value: 1 }, - uAces: { value: 1 }, + uExposure: { value: 1.03 }, + uMap: { value: 1 }, }, }, }, diff --git a/ogl-engine/materials/atmosphere/atmosphere.ts b/ogl-engine/materials/atmosphere/atmosphere.ts index b3a788d9..c9263537 100644 --- a/ogl-engine/materials/atmosphere/atmosphere.ts +++ b/ogl-engine/materials/atmosphere/atmosphere.ts @@ -41,10 +41,10 @@ export class AtmosphereMaterial extends Material { value: new Vec3(), }; this.uniforms.uX = { - value: args.uX ?? -1.96, + value: args.uX ?? -0.3, }; this.uniforms.uY = { - value: args.uY ?? -8.15, + value: args.uY ?? -0.75, }; this.setColor(args.color); diff --git a/ogl-engine/materials/atmosphere/shader.frag.glsl b/ogl-engine/materials/atmosphere/shader.frag.glsl index f61bb76e..e1a28827 100644 --- a/ogl-engine/materials/atmosphere/shader.frag.glsl +++ b/ogl-engine/materials/atmosphere/shader.frag.glsl @@ -27,18 +27,14 @@ void main() { float intensity = 0.0f; for(int i = 0; i < lights.length(); i++) { - if(lights[i].visible) { + if(lights[i].visible && lights[i].position.w == 0.0) { vec3 lightDir = vec3(0.0); - if(lights[i].position.w == 0.0) { - lightDir = normalize(-lights[i].position.xyz); - } else { - continue; - } + lightDir = normalize(-lights[i].position.xyz); float diff = max(dot(norm, lightDir), 0.0); intensity += lights[i].intensity * diff; } } - intensity = clamp(intensity, 0.1f, 1.0f); + intensity = clamp(intensity, 0.1f, 10.0f); fragData[0] = vec4(uColor * density, density * intensity); fragData[1] = fragData[0] * fEmissive; diff --git a/ogl-engine/materials/entityIndicator/shader.frag.glsl b/ogl-engine/materials/entityIndicator/shader.frag.glsl index 94da6a96..284bff81 100644 --- a/ogl-engine/materials/entityIndicator/shader.frag.glsl +++ b/ogl-engine/materials/entityIndicator/shader.frag.glsl @@ -85,5 +85,5 @@ void main() { discard; } - fragData[2] = vec4(color, alpha * (0.75f + sign(float(uSelected)) * 0.22f + sign(float(uHovered)) * 0.1f)); + fragData[2] = vec4(color, clamp(alpha, 0.f, 1.f) * (0.75f + sign(float(uSelected)) * 0.22f + sign(float(uHovered)) * 0.1f)); } \ No newline at end of file diff --git a/ogl-engine/materials/star/shader.frag.glsl b/ogl-engine/materials/star/shader.frag.glsl index 9ca9fe3d..e804b0af 100644 --- a/ogl-engine/materials/star/shader.frag.glsl +++ b/ogl-engine/materials/star/shader.frag.glsl @@ -37,9 +37,9 @@ void main() { vec3 scaledPosition = vec3(vPosition.x * noiseScaleZ, vPosition.y, vPosition.z * noiseScaleZ); float baseNoise = snoise4(vec4(scaledPosition * uNoise, uTime * 0.06f)); float noise = clamp(snoise2(vec2(baseNoise * uNoisePower, uTime * 0.001f)), 0.0f, 0.3f); - vec3 smokeColor = vec3(fbm3(scaledPosition, 7)) + 0.1f; + float smokeColor = fbm3(scaledPosition, 7) + 0.1f; - vec3 color = mix(uColor, uColor2, smokeColor + noise); + vec3 color = pow(mix(uColor, uColor2, smokeColor + noise), vec3(4.f)) * 10000.f; vec3 rimLighting = rim(rimPower, vViewDirection, vWorldNormal, mix(white, uColor, 0.9f)); diff --git a/ogl-engine/post/tonemapping.frag.glsl b/ogl-engine/post/tonemapping.frag.glsl index 09566958..d5db5456 100644 --- a/ogl-engine/post/tonemapping.frag.glsl +++ b/ogl-engine/post/tonemapping.frag.glsl @@ -1,24 +1,26 @@ precision highp float; -#pragma glslify: aces = require("./ogl-engine/shader/aces"); +#pragma glslify: aces = require("glsl-tone-map/aces"); uniform sampler2D tMap; uniform float uGamma; uniform float uSaturation; uniform float uContrast; -uniform float uAces; +uniform float uExposure; +uniform float uMap; varying vec2 vUv; void main() { vec4 color = texture2D(tMap, vUv); - if(uAces > 0.5) { + color.rgb *= uExposure; + + if(uMap > 0.5) { color.rgb = aces(color.rgb); } - color = vec4(color.rgb, 1.); - color = vec4(pow(color.rgb, vec3(1.0 / uGamma)), color.a); - color = (color - 0.5) * uContrast + 0.5; + color.rgb = pow(color.rgb, vec3(1.0 / uGamma)); + color.rgb = (color.rgb - 0.5) * uContrast + 0.5; float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114)); gl_FragColor = mix(vec4(luma, luma, luma, 1.), color, uSaturation); diff --git a/ogl-engine/post/ui.frag.glsl b/ogl-engine/post/ui.frag.glsl index a11ac0f7..edc5635c 100644 --- a/ogl-engine/post/ui.frag.glsl +++ b/ogl-engine/post/ui.frag.glsl @@ -7,5 +7,5 @@ varying vec2 vUv; void main() { vec4 ui = texture2D(tUi, vUv); - gl_FragColor = mix(texture2D(tMap, vUv), ui, ui.a); + gl_FragColor = mix(clamp(texture2D(tMap, vUv), 0., 1.), ui, ui.a); } \ No newline at end of file diff --git a/ogl-engine/shader/aces.glsl b/ogl-engine/shader/aces.glsl deleted file mode 100644 index ca2a4b99..00000000 --- a/ogl-engine/shader/aces.glsl +++ /dev/null @@ -1,11 +0,0 @@ -const float a = 2.51; -const float b = 0.03; -const float c = 2.43; -const float d = 0.59; -const float e = 0.14; - -vec3 aces(vec3 x) { - return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0); -} - -#pragma glslify: export(aces) diff --git a/ogl-engine/shader/pbr.glsl b/ogl-engine/shader/pbr.glsl index fac33681..7f4686ed 100644 --- a/ogl-engine/shader/pbr.glsl +++ b/ogl-engine/shader/pbr.glsl @@ -14,30 +14,27 @@ vec4 pbr(vec3 albedo, vec3 norm, float metallic, float roughness, vec3 emissive, F0 = mix(F0, albedo, metallic); for(int i = 0; i < lights.length(); i++) { - if(lights[i].visible) { - vec3 lightDir = vec3(0.0); - float intensity = lights[i].intensity; - if(lights[i].position.w == 0.0) { - lightDir = normalize(-lights[i].position.xyz); - } else { - vec3 d = lights[i].position.xyz - worldPosition; - lightDir = normalize(d); - float dist = length(d); - intensity /= 1.0 + dist + pow(dist, 2.0); - if(intensity < 0.01) { - continue; - } - } - float diff = max(dot(norm, lightDir), 0.0); - vec3 color = lights[i].color * intensity; - diffuse += diff * color; - specular += cookTorranceSpec(lightDir, eyeDirection, norm, roughness, luma(F0)) * color; + if(!lights[i].visible) { + continue; } - } - float specularPower = luma(specular); - if(specularPower < 0.9) { - specularPower = 0.; + vec3 lightDir = vec3(0.0); + float intensity = lights[i].intensity; + if(lights[i].position.w == 0.0) { + lightDir = normalize(-lights[i].position.xyz); + } else { + vec3 d = lights[i].position.xyz - worldPosition; + lightDir = normalize(d); + float dist = length(d); + intensity /= 1.0 + dist + pow(dist, 2.0); + if(intensity < 0.01) { + continue; + } + } + float diff = max(dot(norm, lightDir), 0.0); + vec3 color = lights[i].color * intensity; + diffuse += diff * color; + specular += cookTorranceSpec(lightDir, eyeDirection, norm, roughness, luma(F0)) * color; } vec3 reflectedDir = reflect(-eyeDirection, norm); diff --git a/package-lock.json b/package-lock.json index 04a7c33c..2ce6c8b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "glsl-noise": "^0.0.0", "glsl-specular-blinn-phong": "^1.0.2", "glsl-specular-cook-torrance": "^2.0.1", + "glsl-tone-map": "^2.1.1", "graphlib": "^2.1.8", "howler": "^2.2.4", "husky": "^8.0.3", @@ -14928,6 +14929,26 @@ "xtend": ">=4.0.0 <4.1.0-0" } }, + "node_modules/glsl-tone-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/glsl-tone-map/-/glsl-tone-map-2.1.1.tgz", + "integrity": "sha512-p+I4CUdRU6E8xqTh1aM5NTqD5z+dnvhU6qtivJA9jG1rtkOvLHhxH2Pe4uKcmQ9u9gDV9ILKZFvVz9Hu5KGrWg==", + "funding": [ + { + "type": "individual", + "url": "https://paypal.me/dmnsgn" + }, + { + "type": "individual", + "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3" + } + ], + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1", + "snowdev": "2.0.0-alpha.28" + } + }, "node_modules/glslify": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", @@ -43242,6 +43263,11 @@ } } }, + "glsl-tone-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/glsl-tone-map/-/glsl-tone-map-2.1.1.tgz", + "integrity": "sha512-p+I4CUdRU6E8xqTh1aM5NTqD5z+dnvhU6qtivJA9jG1rtkOvLHhxH2Pe4uKcmQ9u9gDV9ILKZFvVz9Hu5KGrWg==" + }, "glslify": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", diff --git a/package.json b/package.json index 4e539445..4c7f3376 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "glsl-noise": "^0.0.0", "glsl-specular-blinn-phong": "^1.0.2", "glsl-specular-cook-torrance": "^2.0.1", + "glsl-tone-map": "^2.1.1", "graphlib": "^2.1.8", "howler": "^2.2.4", "husky": "^8.0.3", From 6ec8a7b9f0a51f237526b05695aa6d5f984d97ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Thu, 3 Jul 2025 23:44:41 +0200 Subject: [PATCH 3/4] Update typescript --- ogl-engine/engine/engine3d.ts | 2 ++ package-lock.json | 14 +++++++------- package.json | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ogl-engine/engine/engine3d.ts b/ogl-engine/engine/engine3d.ts index 64781216..bd98fa08 100644 --- a/ogl-engine/engine/engine3d.ts +++ b/ogl-engine/engine/engine3d.ts @@ -123,8 +123,10 @@ export class Engine3D extends Engine { this.renderTarget = new RenderTarget(gl, { // Color, bloom and UI color: 3, + // @ts-expect-error type resolution fails for some reason type: gl.HALF_FLOAT, format: gl.RGBA, + // @ts-expect-error type resolution fails for some reason internalFormat: gl.RGBA16F, }); diff --git a/package-lock.json b/package-lock.json index 2ce6c8b0..6d3987cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -129,7 +129,7 @@ "tsconfig-paths": "^4.2.0", "tsconfig-paths-webpack-plugin": "^4.0.0", "tsx": "^4.19.2", - "typescript": "^5.4.3", + "typescript": "^5.8.3", "webpack": "^5.58.2", "webpack-bugsnag-plugins": "^1.8.0", "webpack-cli": "^4.9.0", @@ -30989,9 +30989,9 @@ } }, "node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -55056,9 +55056,9 @@ } }, "typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true }, "typical": { diff --git a/package.json b/package.json index 4c7f3376..dfcfefad 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "tsconfig-paths": "^4.2.0", "tsconfig-paths-webpack-plugin": "^4.0.0", "tsx": "^4.19.2", - "typescript": "^5.4.3", + "typescript": "^5.8.3", "webpack": "^5.58.2", "webpack-bugsnag-plugins": "^1.8.0", "webpack-cli": "^4.9.0", From 0e69581084eab01d98090a9307e7a740ad090965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Fri, 4 Jul 2025 01:00:55 +0200 Subject: [PATCH 4/4] Add rendering observability tools --- ogl-engine/OglCanvas.tsx | 104 ++++++++++------------ ogl-engine/engine/BaseMesh.ts | 17 +--- ogl-engine/engine/engine3d.ts | 53 ++++++----- ogl-engine/engine/performance.ts | 75 ++++++++++++++++ ogl-engine/useFps.ts | 14 ++- ui/components/MapOverlay/StrategicMap.tsx | 2 +- 6 files changed, 159 insertions(+), 106 deletions(-) create mode 100644 ogl-engine/engine/performance.ts diff --git a/ogl-engine/OglCanvas.tsx b/ogl-engine/OglCanvas.tsx index e83db237..2ab3ce86 100644 --- a/ogl-engine/OglCanvas.tsx +++ b/ogl-engine/OglCanvas.tsx @@ -1,69 +1,57 @@ import React from "react"; -import styles from "./styles.scss"; -import { useFps } from "./useFps"; import type { Engine } from "./engine/engine"; export interface OglCanvasProps { engine: Engine; - fpsCounter?: boolean; } -export const OglCanvas: React.FC = React.memo( - ({ engine, fpsCounter = true }) => { - const [canvas, setCanvas] = React.useState(null); - const resizeObserver = React.useRef(); - const frameIdRef = React.useRef(0); - const { fps, tick, enabled: fpsCounterEnabled } = useFps(fpsCounter); - const [errorCount, setErrorCount] = React.useState(0); +export const OglCanvas: React.FC = React.memo(({ engine }) => { + const [canvas, setCanvas] = React.useState(null); + const resizeObserver = React.useRef(); + const frameIdRef = React.useRef(0); + const [errorCount, setErrorCount] = React.useState(0); - React.useEffect(() => { - const cleanup = () => { - resizeObserver.current?.disconnect(); - cancelAnimationFrame(frameIdRef.current); - }; - if (!canvas) return cleanup; - - engine.hooks.onUpdate.subscribe("OglCanvas", () => { - tick(); - frameIdRef.current = requestAnimationFrame(engine.update.bind(engine)); - }); - engine.hooks.onError.subscribe("OglCanvas", () => { - setErrorCount((count) => count + 1); - }); - - engine.init(canvas); - - resizeObserver.current = new ResizeObserver(engine.resize); - resizeObserver.current.observe(canvas!.parentElement!); + React.useEffect(() => { + const cleanup = () => { + resizeObserver.current?.disconnect(); + cancelAnimationFrame(frameIdRef.current); + }; + if (!canvas) return cleanup; + engine.hooks.onUpdate.subscribe("OglCanvas", () => { frameIdRef.current = requestAnimationFrame(engine.update.bind(engine)); - - setTimeout(engine.resize.bind(engine), 200); - - return cleanup; - }, [canvas, engine]); - - React.useEffect(() => { - if (errorCount > 10) { - console.error("Too many errors, stopping rendering"); - cancelAnimationFrame(frameIdRef.current); - } - }, [errorCount]); - - return ( - <> - { - event.preventDefault(); - }} - style={{ pointerEvents: "all" }} - /> - {fpsCounterEnabled && fpsCounter && ( -
{fps}
- )} - - ); - } -); + }); + engine.hooks.onError.subscribe("OglCanvas", () => { + setErrorCount((count) => count + 1); + }); + + engine.init(canvas); + + resizeObserver.current = new ResizeObserver(engine.resize); + resizeObserver.current.observe(canvas!.parentElement!); + + frameIdRef.current = requestAnimationFrame(engine.update.bind(engine)); + + setTimeout(engine.resize.bind(engine), 200); + + return cleanup; + }, [canvas, engine]); + + React.useEffect(() => { + if (errorCount > 10) { + console.error("Too many errors, stopping rendering"); + cancelAnimationFrame(frameIdRef.current); + } + }, [errorCount]); + + return ( + { + event.preventDefault(); + }} + style={{ pointerEvents: "all" }} + /> + ); +}); diff --git a/ogl-engine/engine/BaseMesh.ts b/ogl-engine/engine/BaseMesh.ts index e670032c..66f2ade3 100644 --- a/ogl-engine/engine/BaseMesh.ts +++ b/ogl-engine/engine/BaseMesh.ts @@ -1,5 +1,5 @@ import type { Material } from "@ogl-engine/materials/material"; -import type { AttributeData, Camera, Geometry, GLTF, MeshOptions } from "ogl"; +import type { AttributeData, Geometry, GLTF, MeshOptions } from "ogl"; import { Mat4, Mesh, Vec3 } from "ogl"; import { MissingMaterial } from "@ogl-engine/materials/missing/missing"; import type { Destroyable } from "@ogl-engine/types"; @@ -173,21 +173,6 @@ export class BaseMesh } } - draw(options?: { camera?: Camera | undefined } | undefined): void { - const start = performance.now(); - super.draw(options); - const end = performance.now(); - - if (this.engine.capturePerformance) { - this.engine.performanceReport.push({ - id: this.id, - label: this.name, - time: end - start, - parent: (this.parent as any)?.id, - }); - } - } - addBoundingBox() { if (this.children.some((child) => child instanceof BoundingBox)) return; diff --git a/ogl-engine/engine/engine3d.ts b/ogl-engine/engine/engine3d.ts index bd98fa08..02b3a34d 100644 --- a/ogl-engine/engine/engine3d.ts +++ b/ogl-engine/engine/engine3d.ts @@ -4,6 +4,7 @@ import settings from "@core/settings"; import { EntityMesh } from "@ui/components/TacticalMap/EntityMesh"; import { gameStore } from "@ui/state/game"; import { sortBy } from "@fxts/core"; +import { getPane } from "@ui/context/Pane"; import brightPassFragment from "../post/brightPass.frag.glsl"; import blurFragment from "../post/blur.frag.glsl"; import fxaaFragment from "../post/fxaa.frag.glsl"; @@ -18,6 +19,7 @@ import { Camera } from "./Camera"; import { Engine } from "./engine"; import { TacticalMapScene, type Scene } from "./Scene"; import { OnBeforeRenderTask } from "./task"; +import { RenderingPerformance } from "./performance"; const bloomSize = 1.2; const lightsNum = 16; @@ -32,17 +34,7 @@ export class Engine3D extends Engine { fxaa = false; godrays = false; scene: TScene; - /** - * Capture performance metrics for the next frame - */ - willCapturePerformance = false; - capturePerformance = false; - performanceReport: Array<{ - id: number; - time: number; - label: string; - parent: number; - }> = []; + performance = new RenderingPerformance(); fxOwners: Record = {}; @@ -102,6 +94,31 @@ export class Engine3D extends Engine { constructor() { super(); + + const folder = getPane().addOrReplaceFolder({ + title: "Renderer", + expanded: true, + }); + folder.addBinding(this.performance, "fps", { + readonly: true, + interval: 500, + label: "FPS", + }); + folder.addBinding(this.performance, "fps", { + view: "graph", + label: "FPS Graph", + interval: 500, + readonly: true, + }); + folder.addBinding(this.performance, "averageFrameTime", { + view: "graph", + label: "Avg Frame Time [ms]", + interval: 500, + readonly: true, + min: 0, + max: 4, + }); + this.initUniforms(); } @@ -330,11 +347,8 @@ export class Engine3D extends Engine { } render() { - if (this.willCapturePerformance) { - this.capturePerformance = true; - this.willCapturePerformance = false; - } - + this.performance.updateTimeToNextFrame(); + const startTime = performance.now(); this.prepareLighting(); if (this.postProcessing) { @@ -342,8 +356,8 @@ export class Engine3D extends Engine { } else { this.renderSimple(); } + this.performance.updateFrameTime(performance.now() - startTime); - this.capturePerformance = false; this.executeOnBeforeRenderTasks(); } @@ -496,11 +510,6 @@ export class Engine3D extends Engine { super.setScene(scene); } - capture() { - this.willCapturePerformance = true; - this.capturePerformance = false; - } - addOnBeforeRenderTask(task: () => void, priority?: number) { const t = new OnBeforeRenderTask(task, priority); this.onBeforeRenderTasks.push(t); diff --git a/ogl-engine/engine/performance.ts b/ogl-engine/engine/performance.ts new file mode 100644 index 00000000..d3895bc2 --- /dev/null +++ b/ogl-engine/engine/performance.ts @@ -0,0 +1,75 @@ +class CyclicBuffer { + private buffer: Float32Array; + private index: number; + private size: number; + private filled: boolean = false; + + constructor(size: number) { + this.size = size; + this.buffer = new Float32Array(size); + this.index = 1; + } + + add(value: number): void { + this.buffer[this.index] = value; + this.index = (this.index + 1) % this.size; + + if (this.index === 0) { + this.filled = true; + } + } + + avg(): number { + let sum = 0; + + for (let i = 0; i < this.size; i++) { + sum += this.buffer[i]; + } + return sum / this.size; + } + + ready(): boolean { + return this.filled; + } + + reset(): void { + this.buffer.fill(0); + this.index = 0; + } +} + +export class RenderingPerformance { + frameTime: CyclicBuffer; + timeToNextFrame: CyclicBuffer; + lastFrameStartTime: number = 0; + + constructor() { + this.frameTime = new CyclicBuffer(120); + this.timeToNextFrame = new CyclicBuffer(120); + } + + updateFrameTime(time: number): void { + this.frameTime.add(time); + } + updateTimeToNextFrame(): void { + const now = performance.now(); + this.timeToNextFrame.add(now - this.lastFrameStartTime); + this.lastFrameStartTime = now; + } + + get fps(): number { + if (!this.frameTime.ready()) { + return 0; + } + + return Math.floor(Math.min(1000 / this.timeToNextFrame.avg(), 240)); + } + + get averageFrameTime(): number { + if (!this.frameTime.ready()) { + return 0; + } + + return this.frameTime.avg(); + } +} diff --git a/ogl-engine/useFps.ts b/ogl-engine/useFps.ts index 9aa12637..1ac9a487 100644 --- a/ogl-engine/useFps.ts +++ b/ogl-engine/useFps.ts @@ -1,10 +1,10 @@ import { actionLoader } from "@core/actionLoader"; import { isDev } from "@core/settings"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; export function useFps(register?: boolean) { - const framesCounterRef = useRef(0); const [fps, setFps] = useState(0); + const [frameTime, setFrameTime] = useState(0); const [enabled, setEnabled] = useState(isDev); useEffect(() => { @@ -23,8 +23,8 @@ export function useFps(register?: boolean) { } const ticker = setInterval(() => { - setFps(framesCounterRef.current); - framesCounterRef.current = 0; + setFps(window.renderer.performance.getFps()); + setFrameTime(window.renderer.performance.getAverageFrameTime()); }, 1000) as unknown as number; return () => { @@ -32,13 +32,9 @@ export function useFps(register?: boolean) { }; }, []); - const tick = useCallback(() => { - framesCounterRef.current += 1; - }, []); - return { fps, + frameTime, enabled, - tick, }; } diff --git a/ui/components/MapOverlay/StrategicMap.tsx b/ui/components/MapOverlay/StrategicMap.tsx index 8d525069..2430be3e 100644 --- a/ui/components/MapOverlay/StrategicMap.tsx +++ b/ui/components/MapOverlay/StrategicMap.tsx @@ -304,6 +304,6 @@ export class StrategicMap extends React.PureComponent { } render() { - return ; + return ; } }