diff --git a/core/systems/moving.ts b/core/systems/moving.ts index acc92181..4e1e486f 100644 --- a/core/systems/moving.ts +++ b/core/systems/moving.ts @@ -4,6 +4,7 @@ import type { TransformData } from "@core/components/transform"; import type { Sim } from "../sim"; import type { RequireComponent } from "../tsHelpers"; import { System } from "./system"; +import { transport3D } from "./transport3d"; const baseMaxSpeed = 8; @@ -77,6 +78,7 @@ export class MovingSystem extends System { this.move(entity, delta); } } + transport3D.publish({ type: "movingSystemFinished" }); } } diff --git a/core/systems/transport3d.ts b/core/systems/transport3d.ts index 9a7e027a..4ce8ded2 100644 --- a/core/systems/transport3d.ts +++ b/core/systems/transport3d.ts @@ -22,12 +22,16 @@ export type DeployFacilityEvent = { type: "deployFacility"; entity: RequireComponent<"position">; }; +export type MovingSystemFinishedEvent = { + type: "movingSystemFinished"; +}; export type Event3D = | ShootEvent | ExplodeEvent | StartMiningEvent | StopMiningEvent - | DeployFacilityEvent; + | DeployFacilityEvent + | MovingSystemFinishedEvent; export const transport3D = new PubSub(); diff --git a/core/utils/misc.ts b/core/utils/misc.ts index a66a23e6..0ae420df 100644 --- a/core/utils/misc.ts +++ b/core/utils/misc.ts @@ -64,3 +64,7 @@ export function isVec2(v: object): v is { value: number[] } { export function lerp(a: number, b: number, t: number): number { return a + (b - a) * t; } + +export function triangle(a: number): number { + return 1 - Math.abs(((a / 2) % 1) * 2 - 1); +} diff --git a/ogl-engine/RibbonEmitter.ts b/ogl-engine/RibbonEmitter.ts index a8f3d758..39c86cd9 100644 --- a/ogl-engine/RibbonEmitter.ts +++ b/ogl-engine/RibbonEmitter.ts @@ -1,13 +1,17 @@ import type { OGLRenderingContext } from "ogl"; import { Geometry, Vec3 } from "ogl"; +import { triangle } from "@core/utils/misc"; import { BaseMesh } from "./engine/BaseMesh"; import { EngineTrailMaterial } from "./materials/engineTrail/engineTrail"; -import type { OnBeforeRenderTask } from "./engine/task"; const tempVec3 = new Vec3(); const tempVec32 = new Vec3(); +const tempVec33 = new Vec3(); +const tempVec34 = new Vec3(); const tempDir = new Vec3(); -const tempNormal = new Vec3(); +const tempNormalForward = new Vec3(); +const tempNormalBackward = new Vec3(); +const up = new Vec3(0, 1, 0); export class RibbonGeometry extends Geometry { readonly maxSegments: number; @@ -18,23 +22,22 @@ export class RibbonGeometry extends Geometry { segments?: Float32Array, width = 0.3 ) { - const position = new Float32Array((maxSegments - 1) * 4 * 3); - const uv = new Float32Array((maxSegments - 1) * 4 * 2); - const index = new Uint16Array((maxSegments - 1) * 6); + const position = new Float32Array(maxSegments * 2 * 3 * 2); + const uv = new Float32Array(maxSegments * 2 * 2 * 2); + const index = new Uint16Array(position.length / 3); if (segments) { - RibbonGeometry.build(segments, position, new Vec3(0, 1, 0), width); + RibbonGeometry.build(segments, position, width); } - for (let i = 0; i < maxSegments - 1; i++) { - index.set( - [i * 4, i * 4 + 2, i * 4 + 1, i * 4 + 2, i * 4 + 3, i * 4 + 1], - i * 6 - ); + for (let i = 0; i < index.length; i++) { + index[i] = i; + } - const uvOffset = i * 2 * 4; - const xStart = i / (maxSegments - 1); - const xEnd = (i + 1) / (maxSegments - 1); + for (let i = 0; i < maxSegments * 2; i++) { + const uvOffset = i * 2 * 2; + const xStart = triangle(i / maxSegments); + const xEnd = triangle((i + 1) / maxSegments); uv[uvOffset] = xStart; uv[uvOffset + 1] = 0; @@ -68,30 +71,64 @@ export class RibbonGeometry extends Geometry { static build( segments: Float32Array, position: Float32Array, - cameraDirection: Vec3, - width: number + width: number, + lengthOverride?: number ) { - const length = segments.length / 4; - for (let i = 0; i < length - 1; i++) { - const segment = segments.subarray(i * 4, i * 4 + 4); - const nextSegment = segments.subarray((i + 1) * 4, (i + 1) * 4 + 4); - - const dir = tempDir - .fromArray(nextSegment) - .sub(tempVec32.set(segment[0], segment[1], segment[2])) + for (let i = 0; i < (lengthOverride ?? segments.length / 4); i++) { + const segment = tempVec32.fromArray( + segments.subarray(i * 4, (i + 1) * 4) + ); + const prevSegment = tempVec33.fromArray( + segments.subarray((i - 1) * 4, i * 4) + ); + const nextSegment = tempVec34.fromArray( + segments.subarray((i + 1) * 4, (i + 2) * 4) + ); + + const dir = tempDir.copy(nextSegment).sub(segment).normalize(); + const normalForward = tempNormalForward.cross(dir, up).normalize(); + const dirBackward = tempDir.copy(segment).sub(prevSegment).normalize(); + const normalBackward = tempNormalBackward + .cross(dirBackward, up) .normalize(); - const normal = tempNormal - .cross(dir, cameraDirection) - .normalize() - .multiply(width); - - const vertOffset = i * 3 * 4; - position.set(tempVec3.set(tempVec32).add(normal), vertOffset); - position.set(tempVec3.set(tempVec32).sub(normal), vertOffset + 3); - position.set(tempVec3.fromArray(nextSegment).add(normal), vertOffset + 6); - position.set(tempVec3.fromArray(nextSegment).sub(normal), vertOffset + 9); + + let normal = normalForward; + if (i === segments.length / 4 - 1) { + normal = normalBackward; + } else if (i > 0) { + normal = normalForward.add(normalBackward).scale(0.5).normalize(); + } + normal.multiply(width); + + const vertOffset = i * 3 * 2; + position.set(tempVec3.copy(segment).add(normal), vertOffset); + position.set(tempVec3.copy(segment).sub(normal), vertOffset + 3); + + normal.copy(up).scale(width); + + position.set( + tempVec3.copy(segment).add(normal), + position.length - 3 - vertOffset + ); + position.set( + tempVec3.copy(segment).sub(normal), + position.length - 3 - (vertOffset + 3) + ); } } + + static addSegment( + segments: Float32Array, + position: Float32Array, + width: number + ) { + position.set(position.subarray(0, position.length / 2 - 3 * 4), 3 * 4); + position.set( + position.subarray(position.length / 2 + 3 * 4), + position.length / 2 + ); + RibbonGeometry.build(segments, position, width, 3); + } } export class RibbonEmitter extends BaseMesh { @@ -101,7 +138,6 @@ export class RibbonEmitter extends BaseMesh { width = 0.3; maxSegments = 25; initialised = false; - task: OnBeforeRenderTask; constructor( trackedEntity: BaseMesh, @@ -115,6 +151,7 @@ export class RibbonEmitter extends BaseMesh { geometry: new RibbonGeometry(trackedEntity.engine.gl, maxSegments), material: new EngineTrailMaterial(trackedEntity.engine, "#ff00ff"), calculateTangents: false, + mode: trackedEntity.engine.gl.TRIANGLE_STRIP, }); this.maxSegments = maxSegments; this.width = width; @@ -122,10 +159,6 @@ export class RibbonEmitter extends BaseMesh { this.offset = offset || new Vec3(); this.engine = trackedEntity.engine; this.segments = new Float32Array(maxSegments * 4); - - this.task = this.engine.addOnBeforeRenderTask(() => { - this.update(this.engine.delta); - }); } update(delta: number) { @@ -157,20 +190,12 @@ export class RibbonEmitter extends BaseMesh { updateGeometry() { if (this.segments.length < 2) return; - RibbonGeometry.build( + RibbonGeometry.addSegment( this.segments, this.geometry.attributes.position.data! as Float32Array, - tempVec3 - .copy(this.trackedEntity.position) - .sub(this.engine.camera.position) - .normalize(), this.width * this.trackedEntity.scale.x ); this.geometry.attributes.position.needsUpdate = true; } - - destroy() { - this.task.cancel(); - } } diff --git a/ogl-engine/engine/Scene.ts b/ogl-engine/engine/Scene.ts index 6c12d109..4a05ad68 100644 --- a/ogl-engine/engine/Scene.ts +++ b/ogl-engine/engine/Scene.ts @@ -187,6 +187,13 @@ export class TacticalMapScene extends Scene { max: 1, } ); + this.pane + .addButton({ + title: "Toggle UI", + }) + .on("click", () => + this.engine.togglePostProcessingPass("composite", "ui") + ); } destroy() { diff --git a/ogl-engine/materials/color/color.ts b/ogl-engine/materials/color/color.ts index 57bb3248..a7607e51 100644 --- a/ogl-engine/materials/color/color.ts +++ b/ogl-engine/materials/color/color.ts @@ -1,3 +1,4 @@ +import type { ProgramOptions } from "ogl"; import { Vec3, Program } from "ogl"; import type { Engine3D } from "@ogl-engine/engine/engine3d"; import type { Engine2D } from "@ogl-engine/engine/engine2d"; @@ -22,18 +23,20 @@ export class ColorMaterial extends Material { opts: { color?: string; shaded?: boolean; - } = {} + layer?: string; + } & Partial> = {} ) { super(engine); - this.createProgram( - vertex, - fragment, - {}, - { - cullFace: false, - } - ); + const defines: Record = {}; + if (opts.layer) { + defines.LAYER = opts.layer; + } + + this.createProgram(vertex, fragment, defines, { + cullFace: false, + ...opts, + }); this.uniforms.uColor = { value: new Vec3() }; this.uniforms.fEmissive = { value: 0 }; diff --git a/ogl-engine/materials/color/shader.frag.glsl b/ogl-engine/materials/color/shader.frag.glsl index 6a531957..7a110e98 100644 --- a/ogl-engine/materials/color/shader.frag.glsl +++ b/ogl-engine/materials/color/shader.frag.glsl @@ -1,6 +1,11 @@ #version 300 es precision highp float; +#pragma defines +#ifndef LAYER +#define LAYER 0 +#endif + #pragma glslify: blinnPhongSpec = require(glsl-specular-blinn-phong) #pragma glslify: Light = require("./ogl-engine/shader/light"); @@ -46,12 +51,13 @@ void main() { } } - fragData[0].rgb = (diffuse + ambient + specular) * uColor; + fragData[LAYER].rgb = (diffuse + ambient + specular) * uColor; } else { - fragData[0] = vec4(uColor, 1.f); + fragData[LAYER] = vec4(uColor, 1.f); } - fragData[0].a = 1.0f; - fragData[1] = vec4(fragData[0].xyz, fEmissive); + fragData[LAYER].a = 1.0f; + fragData[1] = vec4(fragData[LAYER].xyz, fEmissive); + gl_FragDepth = log2(vFragDepth) / uCameraScale; } \ No newline at end of file diff --git a/ogl-engine/materials/engineTrail/shader.frag.glsl b/ogl-engine/materials/engineTrail/shader.frag.glsl index 05044269..e3e5e1b6 100644 --- a/ogl-engine/materials/engineTrail/shader.frag.glsl +++ b/ogl-engine/materials/engineTrail/shader.frag.glsl @@ -10,15 +10,15 @@ uniform vec3 uColor; uniform float uCameraScale; #define emissive 0.9f -#define K 0.02 +#define K 0.01f void main() { - float alphaX = 1.f - vUv.x; + float alphaX = pow(1.f - vUv.x, 3.f); float alphaFactorY = (vUv.y - 0.5f) * 2.0f; float alphaY = K / (K + alphaFactorY * alphaFactorY) - K; - float alpha = alphaX * alphaY; + float alpha = clamp(alphaX * alphaY - 0.005f, 0.f, 0.85f); - fragData[0] = vec4(mix(uColor, vec3(1.f), pow(alphaY, 2.f) - 0.4f), alpha); + fragData[0] = vec4(mix(uColor, vec3(1.f), pow(alphaY, 2.f) - 0.1f), alpha); fragData[1] = vec4(fragData[0].rgb, emissive * alpha); gl_FragDepth = log2(vFragDepth) / uCameraScale; } \ No newline at end of file diff --git a/ogl-engine/materials/engineTrail/shader.vert.glsl b/ogl-engine/materials/engineTrail/shader.vert.glsl index f975092f..f4904daa 100644 --- a/ogl-engine/materials/engineTrail/shader.vert.glsl +++ b/ogl-engine/materials/engineTrail/shader.vert.glsl @@ -12,6 +12,7 @@ uniform mat4 projectionMatrix; void main() { vUv = uv; + gl_Position = projectionMatrix * viewMatrix * vec4(position, 1.0f); vFragDepth = 1.0f + gl_Position.w; } \ No newline at end of file diff --git a/ogl-engine/materials/material.ts b/ogl-engine/materials/material.ts index 05a75270..be63083d 100644 --- a/ogl-engine/materials/material.ts +++ b/ogl-engine/materials/material.ts @@ -4,7 +4,7 @@ import { entries, join, map, pipe } from "@fxts/core"; import type { Engine3D } from "@ogl-engine/engine/engine3d"; import type { Light } from "@ogl-engine/engine/Light"; import Color from "color"; -import type { ProgramOptions, Mesh, Vec3, Vec4, Mat4 } from "ogl"; +import type { ProgramOptions, Mesh, Vec3, Vec4, Mat4, Vec2 } from "ogl"; import { Texture, Program } from "ogl"; import type { BindingParams, FolderApi } from "tweakpane"; import { getPane } from "@ui/context/Pane"; @@ -28,6 +28,7 @@ export abstract class Material { uTime: number; tEnvMap: Texture; uCameraScale: number; + uResolution: Vec2; invProjectionMatrix: Mat4; invViewMatrix: Mat4; invViewProjectionMatrix: Mat4; @@ -43,6 +44,7 @@ export abstract class Material { uTime: engine.uniforms.uTime, tEnvMap: engine.uniforms.env.tEnvMap, uCameraScale: { value: Math.log2(engine.camera.far) }, + uResolution: engine.uniforms.resolution.base, invProjectionMatrix: { value: engine.camera.invProjectionMatrix }, invViewMatrix: { value: engine.camera.invViewMatrix }, invViewProjectionMatrix: { value: engine.camera.invViewProjectionMatrix }, diff --git a/ogl-engine/stories/ribbons.stories.tsx b/ogl-engine/stories/ribbons.stories.tsx index 1a4aad04..ff0b3f77 100644 --- a/ogl-engine/stories/ribbons.stories.tsx +++ b/ogl-engine/stories/ribbons.stories.tsx @@ -4,7 +4,7 @@ import { Styles } from "@kit/theming/style"; import { merge } from "lodash"; import { BaseMesh } from "@ogl-engine/engine/BaseMesh"; import type { Engine3D } from "@ogl-engine/engine/engine3d"; -import { Plane } from "ogl"; +import { Plane, Sphere } from "ogl"; import { RibbonGeometry } from "@ogl-engine/RibbonEmitter"; import { ColorMaterial } from "@ogl-engine/materials/color/color"; import { EngineTrailMaterial } from "@ogl-engine/materials/engineTrail/engineTrail"; @@ -15,7 +15,7 @@ const n = 50; const segments = new Float32Array(n * 4); for (let i = 0; i < n; i++) { - segments.set([i / n, 0, Math.sin(i) / n], i * 4); + segments.set([i / n, 0, Math.sin(i) / n, i], i * 4); } interface RibbonsStoryProps extends Story3dArgs {} @@ -27,13 +27,10 @@ const RibbonsStory: React.FC = (props) => { engine.camera.position.set(1, 1, 1); const ribbon = new BaseMesh(engine, { - geometry: new RibbonGeometry( - engine.gl, - segments.length / 4, - segments, - 0.05 - ), + geometry: new RibbonGeometry(engine.gl, n, segments, 0.05), material: new EngineTrailMaterial(engine, "#ff00ff"), + mode: engine.gl.TRIANGLE_STRIP, + calculateTangents: false, }); ribbon.setParent(engine.scene); @@ -46,6 +43,20 @@ const RibbonsStory: React.FC = (props) => { }); plane.scale.set(0.1); + for (let i = 0; i < n; i++) { + const segment = segments.subarray(i * 4, (i + 1) * 4); + const sphere = new BaseMesh(engine, { + geometry: new Sphere(engine.gl), + material: new ColorMaterial(engine, { + color: "#ffffff", + shaded: false, + }), + }); + sphere.scale.set(0.004); + sphere.position.set(segment[0], segment[1], segment[2]); + engine.scene.addChild(sphere); + } + engine.scene.addChild(plane); }, []); diff --git a/ogl-engine/utils/path.ts b/ogl-engine/utils/path.ts index ac801461..5e264895 100644 --- a/ogl-engine/utils/path.ts +++ b/ogl-engine/utils/path.ts @@ -36,6 +36,9 @@ export class Path extends Transform { material: new ColorMaterial(this.engine, { color: colors.default, shaded: false, + layer: "2", + depthWrite: false, + depthTest: false, }), }); plane.rotation.x = Math.PI / 2; diff --git a/ui/components/TacticalMap/EntityMesh.ts b/ui/components/TacticalMap/EntityMesh.ts index d1cb0d4d..d566025c 100644 --- a/ui/components/TacticalMap/EntityMesh.ts +++ b/ui/components/TacticalMap/EntityMesh.ts @@ -13,12 +13,13 @@ import type { Faction } from "@core/archetypes/faction"; import font from "@assets/fonts/FiraSans/FiraSans-Light.json"; import { EntityNameMaterial } from "@ogl-engine/materials/entityName/entityName"; import type { DockSize } from "@core/components/dockable"; -import { taskPriority, type OnBeforeRenderTask } from "@ogl-engine/engine/task"; +import { type OnBeforeRenderTask } from "@ogl-engine/engine/task"; import models from "@assets/models"; import type { Material } from "@ogl-engine/materials/material"; import { materials } from "@ogl-engine/materials"; import { LaserWeaponEffect } from "@ogl-engine/builders/LaserWeapon"; import type { Turret } from "@core/archetypes/turret"; +import { transport3D } from "@core/systems/transport3d"; const tempVec3 = new Vec3(); export const distanceScale = 250; @@ -134,10 +135,10 @@ export class EntityMesh extends BaseMesh { this.entity = entity; this.name = `EntityMesh:${entity.id}`; - this.tasks.push( - this.engine.addOnBeforeRenderTask( - this.updatePosition.bind(this), - taskPriority.high + this.onDestroyCallbacks.push( + transport3D.subscribe( + "movingSystemFinished", + this.updatePosition.bind(this) ) ); @@ -157,9 +158,9 @@ export class EntityMesh extends BaseMesh { this, input.position, input.scale.x * 4, - { small: 50, medium: 15, large: 10 }[ + { small: 15, medium: 8, large: 12 }[ entity.cp.dockable?.size ?? "large" - ] + ] * 10 ); const color = entity.cp.owner ? entity.sim.getOrThrow(entity.cp.owner.id).cp.color.value @@ -221,6 +222,12 @@ export class EntityMesh extends BaseMesh { this.rotation.y = -this.entity.cp.position.angle; this.setVisibility(!this.entity.cp.render.hidden); this.updateMatrixWorld(); + + for (const child of this.children) { + if (child instanceof RibbonEmitter) { + child.update(this.entity.sim.delta); + } + } } addParticleGenerator(input: ParticleGeneratorInput) {