Update physics

This commit is contained in:
schmelczerandras 2020-10-02 15:12:30 +02:00
parent 1b7dee4be0
commit d9d8d03c36
30 changed files with 447 additions and 552 deletions

View file

@ -23,6 +23,7 @@
"unused-imports/no-unused-imports-ts": "error", "unused-imports/no-unused-imports-ts": "error",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off" "@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off"
} }
} }

View file

@ -12,10 +12,10 @@
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" /> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="icon" href="/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/favicon.ico" type="image/x-icon" />
<title>Test</title> <title>Decla.red</title>
</head> </head>
<body> <body>
<noscript><h1>Javascript is required for this website.</h1></noscript> <noscript>Javascript is required for this website.</noscript>
<div id="overlay"></div> <div id="overlay"></div>
<canvas id="main"></canvas> <canvas id="main"></canvas>
</body> </body>

View file

@ -3,123 +3,9 @@ import { Game } from './scripts/game';
import { Random } from './scripts/helper/random'; import { Random } from './scripts/helper/random';
import './styles/main.scss'; import './styles/main.scss';
glMatrix.setMatrixArrayType( glMatrix.setMatrixArrayType(Array);
Array
); /*
}
}
zeroes.forEach((z) => { const main = async () => {
const blueIndex = z.y * (width * 4) + z.x * 4 + 2;
imageData.data[blueIndex] = 255;
imageData.data[blueIndex - 1] = 255;
imageData.data[blueIndex - 2] = 255;
});
console.log(errors);
ctx.putImageData(imageData, 0, 0);
};*/ //testSDF(); // extract as new image (data-uri)
/*
const testSDF = () => {
Random.seed = 44;
const objects: Array<TunnelShape> = [
new TunnelShape(vec2.fromValues(20, 20), vec2.fromValues(40, 80), 10, 9),
new TunnelShape(vec2.fromValues(40, 80), vec2.fromValues(60, 20), 9, 15),
];
const getPixelValue = (position: vec2) => {
/*const k = 0.15;
let res = Math.pow(2, -k * (vec2.distance(position, vec2.fromValues(20, 20)) - 20));
res += Math.pow(2, -k * (vec2.distance(position, vec2.fromValues(40, 80)) - 30));
res += Math.pow(2, -k * (vec2.distance(position, vec2.fromValues(60, 20)) - 20));
res = -Math.log2(res) / k;
return -res;
*/ /*let min = 1000;
for (const t of objects) {
min = Math.min(min, t.distance(position));
}
if (min < 0) {
// min = Math.min(min, vec2.distance(position, vec2.fromValues(40, 80 - 31.62)) - 31.62);
}
return -min;*/ /*
const tree = new BoundingBoxTree([
new BoundingBox(300, 550, 150, 550, 'A'),
new BoundingBox(400, 800, 50, 200, 'B'),
new BoundingBox(450, 500, 175, 185, 'C'),
new BoundingBox(100, 200, 100, 500, 'D'),
new BoundingBox(750, 950, 450, 600, 'E'),
new BoundingBox(940, 1000, -2, 180, 'F'),
new BoundingBox(960, 1050, 50, 190, 'G'),
new BoundingBox(150, 900, 0, 575, 'H'),
new BoundingBox(-10000, 10000, -10000, 10000, 'I'),
]);
tree.print();
console.log(tree.findIntersecting(new BoundingBox(960, 1050, 50, 190, 'G')));
*/ /*
return -vec2.distance(position, vec2.fromValues(40, 40)) + 30;
};
/*
const width = 80;
const height = 100;
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
// set desired size of transparent image
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(width, height);
const zeroes = [];
for (let x = 0; x < width; x++) {
console.log(x);
for (let y = 0; y < height; y++) {
const position = vec2.fromValues(x, y);
const redIndex = y * (width * 4) + x * 4;
const dist = getPixelValue(position);
if (Math.abs(dist) < 1.5) {
zeroes.push(position);
}
const blueIndex = y * (width * 4) + x * 4 + 2;
//imageData.data[blueIndex] = dist * 10;
imageData.data[redIndex + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
console.log(zeroes);
const errors = [];
for (let x = 0; x < width; x++) {
console.log('verify ', x);
for (let y = 0; y < height; y++) {
const position = vec2.fromValues(x, y);
const dist = getPixelValue(position);
const realDistance = zeroes.map((z) => vec2.distance(z, position)).sort()[0];
if (Math.abs(realDistance - dist) < 3) {
const greenIndex = y * (width * 4) + x * 4 + 1;
imageData.data[greenIndex] = 255;
} else if (dist >= 0) {
const redIndex = y * (width * 4) + x * 4;
errors.push(realDistance - dist);
imageData.data[redIndex] = 255;
}
/*if (zeroes.find((z) => vec2.distance(z, position) < dist) !== undefined) {
const blueIndex = y * (width * 4) + x * 4 + 2;
imageData.data[blueIndex] = 255;
}*/ const main = async () => {
try { try {
Random.seed = 42; Random.seed = 42;
await new Game().start(); await new Game().start();

View file

@ -1,31 +1,30 @@
import { vec3 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { CircleLight, compile, Flashlight, Renderer } from 'sdf-2d'; import { CircleLight, compile, Flashlight, InvertedTunnel, Renderer } from 'sdf-2d';
import { CommandBroadcaster } from './commands/command-broadcaster'; import { CommandBroadcaster } from './commands/command-broadcaster';
import { RenderCommand } from './graphics/commands/render'; import { RenderCommand } from './graphics/commands/render';
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
import { prettyPrint } from './helper/pretty-print'; import { prettyPrint } from './helper/pretty-print';
import { rgb } from './helper/rgb';
import { IGame } from './i-game'; import { IGame } from './i-game';
import { KeyboardListener } from './input/keyboard-listener'; import { KeyboardListener } from './input/keyboard-listener';
import { MouseListener } from './input/mouse-listener'; import { MouseListener } from './input/mouse-listener';
import { TouchListener } from './input/touch-listener'; import { TouchListener } from './input/touch-listener';
import { GameObject } from './objects/game-object';
import { Objects } from './objects/objects'; import { Objects } from './objects/objects';
import { Camera } from './objects/types/camera'; import { Camera } from './objects/types/camera';
import { Character } from './objects/types/character'; import { Character } from './objects/types/character';
import { createDungeon } from './objects/world/create-dungeon'; import { createDungeon } from './objects/world/create-dungeon';
import { BoundingBoxBase } from './physics/bounds/bounding-box-base';
import { MoveToCommand } from './physics/commands/move-to'; import { MoveToCommand } from './physics/commands/move-to';
import { StepCommand } from './physics/commands/step'; import { StepCommand } from './physics/commands/step';
import { TeleportToCommand } from './physics/commands/teleport-to'; import { TeleportToCommand } from './physics/commands/teleport-to';
import { Physics } from './physics/physics'; import { Physics } from './physics/physics';
import { BoundingBoxBase } from './shapes/bounding-box-base'; import { settings } from './settings';
import { CircleShape } from './shapes/types/circle-shape'; import { BlobShape } from './shapes/types/blob-shape';
import { TunnelShape } from './shapes/types/tunnel-shape';
export class Game implements IGame { export class Game implements IGame {
public readonly objects = new Objects(); public readonly objects = new Objects();
public readonly physics = new Physics(); public readonly physics = new Physics();
public readonly camera = new Camera(); public readonly camera = new Camera();
private previousTime?: DOMHighResTimeStamp = null;
private previousFpsValues: Array<number> = [];
private character: Character; private character: Character;
private renderer: Renderer; private renderer: Renderer;
private rendererPromise: Promise<Renderer>; private rendererPromise: Promise<Renderer>;
@ -34,8 +33,6 @@ export class Game implements IGame {
constructor() { constructor() {
const canvas: HTMLCanvasElement = document.querySelector('canvas#main'); const canvas: HTMLCanvasElement = document.querySelector('canvas#main');
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
new CommandBroadcaster( new CommandBroadcaster(
[ [
new KeyboardListener(document.body), new KeyboardListener(document.body),
@ -48,16 +45,16 @@ export class Game implements IGame {
this.rendererPromise = compile( this.rendererPromise = compile(
canvas, canvas,
[ [
CircleShape.descriptor, InvertedTunnel.descriptor,
TunnelShape.descriptor,
Flashlight.descriptor, Flashlight.descriptor,
BlobShape.descriptor,
CircleLight.descriptor, CircleLight.descriptor,
], ],
[ {
vec3.fromValues(0, 0, 0), shadowTraceCount: 12,
vec3.fromValues(224 / 255, 96 / 255, 126 / 255), paletteSize: 10,
vec3.fromValues(119 / 255, 173 / 255, 120 / 255), enableStopwatch: true,
] }
); );
this.initializeScene(); this.initializeScene();
this.physics.start(); this.physics.start();
@ -67,16 +64,22 @@ export class Game implements IGame {
this.renderer = await this.rendererPromise; this.renderer = await this.rendererPromise;
this.renderer.setRuntimeSettings({ this.renderer.setRuntimeSettings({
isWorldInverted: true, isWorldInverted: true,
ambientLight: vec3.fromValues(0.35, 0.1, 0.45), ambientLight: rgb(0.35, 0.1, 0.45),
shadowLength: 300, colorPalette: [
rgb(0.4, 1, 0.6),
rgb(1, 1, 0),
rgb(0.3, 1, 1),
rgb(0.3, 1, 1),
rgb(0.3, 1, 1),
rgb(0.3, 1, 1),
rgb(0.3, 1, 1),
],
enableHighDpiRendering: false,
lightCutoffDistance: settings.lightCutoffDistance,
}); });
requestAnimationFrame(this.gameLoop.bind(this)); requestAnimationFrame(this.gameLoop.bind(this));
} }
public addObject(o: GameObject) {
this.objects.addObject(o);
}
public get viewArea(): BoundingBoxBase { public get viewArea(): BoundingBoxBase {
return this.camera.viewArea; return this.camera.viewArea;
} }
@ -86,51 +89,38 @@ export class Game implements IGame {
} }
private initializeScene() { private initializeScene() {
const start = createDungeon(this.objects, this.physics);
createDungeon(this.objects, this.physics); createDungeon(this.objects, this.physics);
//createDungeon(this.objects, this.physics);
this.character = new Character(this); this.character = new Character(this.physics, this);
// this.physics.addDynamicBoundingBox(this.character.boundingBox); this.objects.addObject(this.character);
this.addObject(this.character); this.objects.addObject(this.camera);
this.addObject(this.camera); let pos: any = localStorage.getItem('characterPosition');
let pos: any = localStorage.getItem('character-position'); pos = pos ? JSON.parse(pos) : vec2.fromValues(0, 0);
pos = pos ? JSON.parse(pos) : start.from;
this.character.sendCommand(new TeleportToCommand(pos)); this.character.sendCommand(new TeleportToCommand(pos));
} }
private handleVisibilityChange() { private deltaTimeCalculator = new DeltaTimeCalculator();
if (!document.hidden) {
this.previousTime = null;
}
}
private gameLoop(time: DOMHighResTimeStamp) { private gameLoop(time: DOMHighResTimeStamp) {
if (this.previousTime === null) { const deltaTime = this.deltaTimeCalculator.getNextDeltaTime(time);
this.previousTime = time;
}
const deltaTime = time - this.previousTime;
this.previousTime = time;
this.objects.sendCommand(new StepCommand(deltaTime)); this.objects.sendCommand(new StepCommand(deltaTime));
this.camera.sendCommand(new MoveToCommand(this.character.position)); this.camera.sendCommand(new MoveToCommand(this.character.position));
this.camera.sendCommand(new RenderCommand(this.renderer)); this.camera.sendCommand(new RenderCommand(this.renderer));
const shouldBeDrawn = this.physics const shouldBeDrawn = this.physics
.findIntersecting(this.camera.viewArea) .findIntersecting(this.camera.viewArea)
.map((b) => b.shape?.gameObject); .map((b) => b.owner);
for (const object of shouldBeDrawn) { for (const object of shouldBeDrawn) {
object?.sendCommand(new RenderCommand(this.renderer)); object?.sendCommand(new RenderCommand(this.renderer));
} }
this.character.sendCommand(new RenderCommand(this.renderer));
this.renderer.renderDrawables(); this.renderer.renderDrawables();
this.overlay.innerText = prettyPrint(this.renderer.insights); this.overlay.innerText = prettyPrint(this.renderer.insights);
localStorage.setItem('characterPosition', JSON.stringify(this.character.position));
localStorage.setItem('character-position', JSON.stringify(this.character.position));
requestAnimationFrame(this.gameLoop.bind(this)); requestAnimationFrame(this.gameLoop.bind(this));
} }
} }

View file

@ -1,5 +0,0 @@
export const exponentialDecay = (
accumulator: number,
nextValue: number,
biasOfNextValue: number
) => accumulator * (1 - biasOfNextValue) + nextValue * biasOfNextValue;

View file

@ -1,29 +0,0 @@
export const getCombinations = (values: Array<Array<number>>): Array<Array<number>> => {
if (!values.every((a) => a.length > 0)) {
return [];
}
const result: Array<Array<number>> = [];
const counters = values.map((_) => 0);
const increaseCounter = (i: number) => {
if (i >= counters.length) {
return false;
}
counters[i]++;
if (counters[i] >= values[i].length) {
counters[i] = 0;
return increaseCounter(i + 1);
}
return true;
};
do {
result.push(values.map((v, i) => v[counters[i]]));
} while (increaseCounter(0));
return result;
};

View file

@ -0,0 +1,3 @@
import { vec3 } from 'gl-matrix';
export const rgb = (r: number, g: number, b: number): vec3 => vec3.fromValues(r, g, b);

View file

@ -0,0 +1,4 @@
import { vec3 } from 'gl-matrix';
export const rgb255 = (r: number, g: number, b: number): vec3 =>
vec3.fromValues(r / 255, g / 255, b / 255);

View file

@ -1,29 +0,0 @@
export const waitWhileFalse = (body: () => boolean): Promise<void> => {
let resolveOnDone: () => void;
let rejectOnDone: (e: Error) => void;
const onDone = new Promise<void>((resolve, reject) => {
resolveOnDone = resolve;
rejectOnDone = reject;
});
const waiter = () => {
let success: boolean;
try {
success = body();
} catch (e) {
rejectOnDone(e);
return;
}
if (success) {
resolveOnDone();
} else {
requestAnimationFrame(waiter);
}
};
waiter();
return onDone;
};

View file

@ -1,8 +1,6 @@
import { GameObject } from './objects/game-object'; import { BoundingBoxBase } from './physics/bounds/bounding-box-base';
import { BoundingBoxBase } from './shapes/bounding-box-base';
export interface IGame { export interface IGame {
addObject(o: GameObject): void;
readonly viewArea: BoundingBoxBase; readonly viewArea: BoundingBoxBase;
findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase>; findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase>;
} }

View file

@ -2,8 +2,8 @@ import { vec2 } from 'gl-matrix';
import { RenderCommand } from '../../graphics/commands/render'; import { RenderCommand } from '../../graphics/commands/render';
import { CursorMoveCommand } from '../../input/commands/cursor-move-command'; import { CursorMoveCommand } from '../../input/commands/cursor-move-command';
import { ZoomCommand } from '../../input/commands/zoom'; import { ZoomCommand } from '../../input/commands/zoom';
import { BoundingBox } from '../../physics/bounds/bounding-box';
import { MoveToCommand } from '../../physics/commands/move-to'; import { MoveToCommand } from '../../physics/commands/move-to';
import { BoundingBox } from '../../shapes/bounding-box';
import { GameObject } from '../game-object'; import { GameObject } from '../game-object';
export class Camera extends GameObject { export class Camera extends GameObject {

View file

@ -1,41 +1,61 @@
import { vec2, vec3 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { Flashlight } from 'sdf-2d'; import { Flashlight } from 'sdf-2d';
import { RenderCommand } from '../../graphics/commands/render'; import { RenderCommand } from '../../graphics/commands/render';
import { rgb } from '../../helper/rgb';
import { IGame } from '../../i-game'; import { IGame } from '../../i-game';
import { KeyDownCommand } from '../../input/commands/key-down'; import { KeyDownCommand } from '../../input/commands/key-down';
import { KeyUpCommand } from '../../input/commands/key-up'; import { KeyUpCommand } from '../../input/commands/key-up';
import { SwipeCommand } from '../../input/commands/swipe'; import { SwipeCommand } from '../../input/commands/swipe';
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
import { BoundingCircle } from '../../physics/bounds/bounding-circle';
import { StepCommand } from '../../physics/commands/step'; import { StepCommand } from '../../physics/commands/step';
import { TeleportToCommand } from '../../physics/commands/teleport-to'; import { TeleportToCommand } from '../../physics/commands/teleport-to';
import { IShape } from '../../shapes/i-shape'; import { DynamicPhysicalObject } from '../../physics/dynamic-physical-object';
import { Physics } from '../../physics/physics';
import { BlobShape } from '../../shapes/types/blob-shape'; import { BlobShape } from '../../shapes/types/blob-shape';
import { GameObject } from '../game-object';
export class Character extends GameObject { export class Character extends DynamicPhysicalObject {
protected head: BoundingCircle;
protected leftFoot: BoundingCircle;
protected rightFoot: BoundingCircle;
private keysDown: Set<string> = new Set(); private keysDown: Set<string> = new Set();
private light = new Flashlight( private light = new Flashlight(
vec2.create(), vec2.fromValues(0, 0),
vec3.fromValues(1, 0.6, 0.45), rgb(1, 0.6, 0.45),
1.5, 0.5,
vec2.fromValues(-1, 0) vec2.fromValues(-1, 0)
); );
private shape = new BlobShape(vec2.create());
private shape = new BlobShape();
private boundingCircle = new BoundingCircle(this, vec2.create(), 50);
private center = vec2.create();
private static speed = 1.5; private static speed = 1.5;
constructor(private game: IGame) { constructor(physics: Physics, private game: IGame) {
super(); super(physics, true);
this.addCommandExecutor(RenderCommand, this.draw.bind(this)); this.addCommandExecutor(RenderCommand, this.draw.bind(this));
this.addCommandExecutor(TeleportToCommand, (c) => this.setPosition(c.position)); this.addCommandExecutor(TeleportToCommand, (c) => this.setPosition(c.position));
this.addCommandExecutor(KeyDownCommand, (c) => this.keysDown.add(c.key)); this.addCommandExecutor(KeyDownCommand, (c) => this.keysDown.add(c.key));
this.addCommandExecutor(KeyUpCommand, (c) => this.keysDown.delete(c.key)); this.addCommandExecutor(KeyUpCommand, (c) => this.keysDown.delete(c.key));
this.addCommandExecutor(StepCommand, this.stepHandler.bind(this));
this.addCommandExecutor(SwipeCommand, (c) => { this.addCommandExecutor(SwipeCommand, (c) => {
this.tryMoving(vec2.multiply(vec2.create(), c.delta, this.game.viewArea.size)); this.tryMoving(vec2.multiply(vec2.create(), c.delta, this.game.viewArea.size));
}); });
this.head = new BoundingCircle(this, this.center, 50);
this.leftFoot = new BoundingCircle(this, this.center, 50);
this.rightFoot = new BoundingCircle(this, this.center, 50);
this.shape.setCircles([this.head, this.leftFoot, this.rightFoot]);
this.addToPhysics();
} }
public get position(): vec2 { public distance(target: vec2): number {
return this.shape.center; return this.shape.minDistance(target);
} }
private draw(c: RenderCommand) { private draw(c: RenderCommand) {
@ -43,73 +63,24 @@ export class Character extends GameObject {
c.renderer.addDrawable(this.light); c.renderer.addDrawable(this.light);
} }
private tryMoving(delta: vec2, isFirstIteration = true) { public get position(): vec2 {
const maxStep = 2; return this.center;
if (vec2.length(delta) > maxStep) {
let steppedDelta = vec2.normalize(vec2.create(), delta);
vec2.scale(steppedDelta, steppedDelta, maxStep - 0.001);
for (let i = 0; i <= vec2.length(delta) / maxStep - 1; i++) {
this.tryMoving(vec2.clone(steppedDelta), isFirstIteration);
}
steppedDelta = vec2.normalize(vec2.create(), delta);
vec2.scale(steppedDelta, steppedDelta, vec2.length(delta) % maxStep);
this.tryMoving(vec2.clone(steppedDelta), isFirstIteration);
return;
}
const nextShape = this.shape.clone();
vec2.add(nextShape.center, nextShape.center, delta);
const nextNearShapes = this.getNearShapesTo(nextShape);
if (nextNearShapes.length && nextNearShapes[0].distance < 0) {
this.setPosition(nextShape.center);
} else {
if (!isFirstIteration) {
return;
}
const currentNearShapes = this.getNearShapesTo(this.shape);
const intersecting = nextNearShapes
.filter(
(n) =>
currentNearShapes.find((c) => c.shape === n.shape && c.distance <= 0) !==
undefined
)
.sort((e) => Math.abs(e.distance));
if (intersecting.length < 1) {
return;
}
const normal = intersecting[0].shape.normal(this.shape.center);
const maxDistance = intersecting.reduce((p, c) => (p.distance > c.distance ? p : c))
.distance;
vec2.add(delta, delta, vec2.scale(vec2.create(), normal, -maxDistance - 2));
this.tryMoving(delta, false);
}
} }
private getNearShapesTo(shape: BlobShape): Array<{ shape: IShape; distance: number }> { public getBoundingCircles(): Array<BoundingCircle> {
return this.game return [this.boundingCircle];
.findIntersecting(shape.boundingBox) }
.filter((b) => b.shape)
.map((b) => ({ public getBoundingBox(): BoundingBoxBase {
shape: b.shape, return this.head.boundingBox;
// TODO: fix this }
distance: b.shape.minDistance(shape.center) + shape.radius - 20,
})) private tryMoving(delta: vec2) {
.sort((e) => e.distance); this.physics.tryMovingDynamicCircle(this.head, delta);
} }
private setPosition(value: vec2) { private setPosition(value: vec2) {
this.shape.position = value; //this.head.center = value;
this.light.center = vec2.add(vec2.create(), value, vec2.fromValues(50, -40)); this.light.center = vec2.add(vec2.create(), value, vec2.fromValues(50, -40));
} }

View file

@ -1,21 +1,40 @@
import { vec2, vec3 } from 'gl-matrix'; import { vec2, vec3 } from 'gl-matrix';
import { CircleLight } from 'sdf-2d'; import { CircleLight } from 'sdf-2d';
import { RenderCommand } from '../../graphics/commands/render'; import { RenderCommand } from '../../graphics/commands/render';
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
import { ImmutableBoundingBox } from '../../physics/bounds/immutable-bounding-box';
import { MoveToCommand } from '../../physics/commands/move-to'; import { MoveToCommand } from '../../physics/commands/move-to';
import { GameObject } from '../game-object'; import { PhysicalObject } from '../../physics/physical-object';
import { Physics } from '../../physics/physics';
import { settings } from '../../settings';
export class Lamp extends GameObject { export class Lamp extends PhysicalObject {
private light: CircleLight; private light: CircleLight;
constructor(center: vec2, color: vec3, lightness: number) { constructor(center: vec2, color: vec3, lightness: number, physics: Physics) {
super(); super(physics, false);
this.light = new CircleLight(center, color, lightness); this.light = new CircleLight(center, color, lightness);
this.addToPhysics();
this.addCommandExecutor(RenderCommand, this.draw.bind(this)); this.addCommandExecutor(RenderCommand, this.draw.bind(this));
this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this));
} }
public distance(target: vec2): number {
return this.light.minDistance(target);
}
public getBoundingBox(): BoundingBoxBase {
return new ImmutableBoundingBox(
this,
this.light.center.x - settings.lightCutoffDistance,
this.light.center.x + settings.lightCutoffDistance,
this.light.center.y - settings.lightCutoffDistance,
this.light.center.y + settings.lightCutoffDistance
);
}
private draw(c: RenderCommand) { private draw(c: RenderCommand) {
c.renderer.addDrawable(this.light); c.renderer.addDrawable(this.light);
} }

View file

@ -1,26 +1,52 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { RenderCommand } from '../../graphics/commands/render'; import { RenderCommand } from '../../graphics/commands/render';
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
import { ImmutableBoundingBox } from '../../physics/bounds/immutable-bounding-box';
import { Physics } from '../../physics/physics'; import { Physics } from '../../physics/physics';
import { StaticPhysicalObject } from '../../physics/static-physical-object';
import { TunnelShape } from '../../shapes/types/tunnel-shape'; import { TunnelShape } from '../../shapes/types/tunnel-shape';
import { GameObject } from '../game-object';
export class Tunnel extends GameObject { export class Tunnel extends StaticPhysicalObject {
private shape: TunnelShape; private shape: TunnelShape;
constructor( constructor(
physics: Physics, physics: Physics,
public readonly from: vec2, from: vec2,
to: vec2, to: vec2,
fromRadius: number, fromRadius: number,
toRadius: number toRadius: number
) { ) {
super(); super(physics, true);
this.shape = new TunnelShape(from, to, fromRadius, toRadius);
this.addToPhysics();
this.shape = new TunnelShape(from, to, fromRadius, toRadius, this);
physics.addStaticBoundingBox(this.shape.boundingBox);
this.addCommandExecutor(RenderCommand, this.draw.bind(this)); this.addCommandExecutor(RenderCommand, this.draw.bind(this));
} }
public distance(target: vec2): number {
return this.shape.minDistance(target);
}
public getBoundingBox(): BoundingBoxBase {
const xMin = Math.min(
this.shape.from.x - this.shape.fromRadius,
this.shape.to.x - this.shape.toRadius
);
const yMin = Math.min(
this.shape.from.y - this.shape.fromRadius,
this.shape.to.y - this.shape.toRadius
);
const xMax = Math.max(
this.shape.from.x + this.shape.fromRadius,
this.shape.to.x + this.shape.toRadius
);
const yMax = Math.max(
this.shape.from.y + this.shape.fromRadius,
this.shape.to.y + this.shape.toRadius
);
return new ImmutableBoundingBox(this, xMin, xMax, yMin, yMax, true);
}
private draw(c: RenderCommand) { private draw(c: RenderCommand) {
c.renderer.addDrawable(this.shape); c.renderer.addDrawable(this.shape);
} }

View file

@ -1,14 +1,15 @@
import { vec2 } from 'gl-matrix'; import { vec2, vec3 } from 'gl-matrix';
import { Random } from '../../helper/random'; import { Random } from '../../helper/random';
import { Physics } from '../../physics/physics'; import { Physics } from '../../physics/physics';
import { Objects } from '../objects'; import { Objects } from '../objects';
import { Lamp } from '../types/lamp';
import { Tunnel } from '../types/tunnel'; import { Tunnel } from '../types/tunnel';
export const createDungeon = (objects: Objects, physics: Physics): Tunnel => { export const createDungeon = (objects: Objects, physics: Physics) => {
let previousRadius = 350; let previousRadius = 350;
let previousEnd = vec2.create(); let previousEnd = vec2.create();
let first: Tunnel; let tunnelsCountSinceLastLight = 0;
for (let i = 0; i < 500000; i += 500) { for (let i = 0; i < 500000; i += 500) {
const deltaHeight = (Random.getRandom() - 0.5) * 2000; const deltaHeight = (Random.getRandom() - 0.5) * 2000;
@ -24,30 +25,24 @@ export const createDungeon = (objects: Objects, physics: Physics): Tunnel => {
currentToRadius currentToRadius
); );
if (!first) {
first = tunnel;
}
objects.addObject(tunnel); objects.addObject(tunnel);
/* if (deltaHeight > 0 && Random.getRandom() > 0.8) { if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.6) {
objects.addObject( objects.addObject(
new Lamp( new Lamp(
currentEnd, currentEnd,
Random.getRandom() * 20 + 30, vec3.normalize(
vec3.scale(
vec3.create(), vec3.create(),
vec3.normalize(vec3.create(), vec3.fromValues(0.5, 0.1, 0.8)), vec3.fromValues(Random.getRandom(), 0, Random.getRandom())
Random.getRandom() * 0.5 + 0.5
), ),
1 0.5,
physics
) )
); );
} */ tunnelsCountSinceLastLight = 0;
}
previousEnd = currentEnd; previousEnd = currentEnd;
previousRadius = currentToRadius; previousRadius = currentToRadius;
} }
return first;
}; };

View file

@ -1,13 +1,14 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { IShape } from './i-shape'; import { PhysicalObject } from '../physical-object';
export abstract class BoundingBoxBase { export abstract class BoundingBoxBase {
constructor( constructor(
public readonly shape: IShape, public readonly owner: PhysicalObject,
protected _xMin: number = 0, protected _xMin: number = 0,
protected _xMax: number = 0, protected _xMax: number = 0,
protected _yMin: number = 0, protected _yMin: number = 0,
protected _yMax: number = 0 protected _yMax: number = 0,
public readonly isInverted = false
) {} ) {}
public get 0(): number { public get 0(): number {

View file

@ -3,6 +3,38 @@ import { BoundingBoxBase } from './bounding-box-base';
import { ImmutableBoundingBox } from './immutable-bounding-box'; import { ImmutableBoundingBox } from './immutable-bounding-box';
export class BoundingBox extends BoundingBoxBase { export class BoundingBox extends BoundingBoxBase {
public get xMin(): number {
return this._xMin;
}
public set xMin(value: number) {
this._xMin = value;
}
public set xMax(value: number) {
this._xMax = value;
}
public get xMax(): number {
return this._xMax;
}
public set yMin(value: number) {
this._yMin = value;
}
public get yMin(): number {
return this._yMin;
}
public set yMax(value: number) {
this._yMax = value;
}
public get yMax(): number {
return this._yMax;
}
public get topLeft(): vec2 { public get topLeft(): vec2 {
return vec2.fromValues(this._xMin, this._yMax); return vec2.fromValues(this._xMin, this._yMax);
} }
@ -12,18 +44,18 @@ export class BoundingBox extends BoundingBoxBase {
this._yMax = value.y; this._yMax = value.y;
} }
public get size(): vec2 {
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
}
public set size(value: vec2) { public set size(value: vec2) {
this._xMax = this.xMin + value.x; this._xMax = this.xMin + value.x;
this._yMin = this.yMax - value.y; this._yMin = this.yMax - value.y;
} }
public get size(): vec2 {
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
}
public cloneAsImmutable(): ImmutableBoundingBox { public cloneAsImmutable(): ImmutableBoundingBox {
return new ImmutableBoundingBox( return new ImmutableBoundingBox(
this.shape, this.owner,
this.xMin, this.xMin,
this.xMax, this.xMax,
this.yMin, this.yMin,

View file

@ -0,0 +1,67 @@
import { vec2 } from 'gl-matrix';
import { PhysicalObject } from '../physical-object';
import { BoundingBox } from './bounding-box';
import { BoundingBoxBase } from './bounding-box-base';
export class BoundingCircle {
private _boundingBox: BoundingBox;
constructor(public readonly owner, private _center: vec2, private _radius: number) {
this._boundingBox = new BoundingBox(owner);
this.recalculateBoundingBox();
}
public get center(): vec2 {
return this._center;
}
public set center(value: vec2) {
this._center = value;
this.recalculateBoundingBox();
}
public get radius(): number {
return this._radius;
}
public set radius(value: number) {
this._radius = value;
this.recalculateBoundingBox();
}
public distance(target: vec2): number {
return vec2.distance(target, this.center) - this.radius;
}
public areIntersecting(other: PhysicalObject): boolean {
return other.distance(this.center) < this.radius;
}
public isInside(other: PhysicalObject): boolean {
return other.distance(this.center) < -this.radius;
}
public getPerimeterPoints(count: number): Array<vec2> {
const result: Array<vec2> = [];
for (let i = 0; i < count; i++) {
result.push(
vec2.fromValues(
Math.cos((2 * Math.PI * i) / count) * this.radius + this.center.x,
Math.sin((2 * Math.PI * i) / count) * this.radius + this.center.y
)
);
}
return result;
}
public get boundingBox(): BoundingBoxBase {
return this._boundingBox;
}
private recalculateBoundingBox() {
this._boundingBox.xMin = this.center.x - this._radius;
this._boundingBox.xMax = this.center.x + this._radius;
this._boundingBox.yMin = this.center.y - this._radius;
this._boundingBox.yMax = this.center.y + this._radius;
}
}

View file

@ -1,4 +1,4 @@
import { BoundingBoxBase } from '../../shapes/bounding-box-base'; import { BoundingBoxBase } from '../bounds/bounding-box-base';
export class BoundingBoxList { export class BoundingBoxList {
constructor(private boundingBoxes: Array<BoundingBoxBase> = []) {} constructor(private boundingBoxes: Array<BoundingBoxBase> = []) {}

View file

@ -1,12 +1,10 @@
// source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js // source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js
import { ImmutableBoundingBox } from '../../shapes/immutable-bounding-box'; import { ImmutableBoundingBox } from '../bounds/immutable-bounding-box';
class Node { class Node {
public left?: Node = null; public left?: Node = null;
public right?: Node = null; public right?: Node = null;
constructor(public rectangle: ImmutableBoundingBox, public parent: Node) {} constructor(public rectangle: ImmutableBoundingBox, public parent: Node) {}
} }

View file

@ -0,0 +1,10 @@
import { BoundingCircle } from './bounds/bounding-circle';
import { PhysicalObject } from './physical-object';
export abstract class DynamicPhysicalObject extends PhysicalObject {
public abstract getBoundingCircles(): Array<BoundingCircle>;
protected addToPhysics() {
super.addToPhysics(true);
}
}

View file

@ -0,0 +1,21 @@
import { vec2 } from 'gl-matrix';
import { GameObject } from '../objects/game-object';
import { BoundingBoxBase } from './bounds/bounding-box-base';
import { Physics } from './physics';
export abstract class PhysicalObject extends GameObject {
public abstract getBoundingBox(): BoundingBoxBase;
public abstract distance(target: vec2): number;
constructor(protected physics: Physics, public readonly canCollide: boolean) {
super();
}
protected addToPhysics(isDynamic = false) {
if (isDynamic) {
this.physics.addDynamicBoundingBox(this.getBoundingBox());
} else {
this.physics.addStaticBoundingBox(this.getBoundingBox());
}
}
}

View file

@ -1,15 +1,14 @@
import { BoundingBoxTree } from './containers/bounding-box-tree'; import { vec2 } from 'gl-matrix';
import { BoundingBoxBase } from './bounds/bounding-box-base';
import { BoundingCircle } from './bounds/bounding-circle';
import { ImmutableBoundingBox } from './bounds/immutable-bounding-box';
import { BoundingBoxList } from './containers/bounding-box-list'; import { BoundingBoxList } from './containers/bounding-box-list';
import { ImmutableBoundingBox } from '../shapes/immutable-bounding-box'; import { BoundingBoxTree } from './containers/bounding-box-tree';
import { BoundingBoxBase } from '../shapes/bounding-box-base';
export class Physics { export class Physics {
private isTreeInitialized = false; private isTreeInitialized = false;
private staticBoundingBoxesWaitList = []; private staticBoundingBoxesWaitList = [];
private staticBoundingBoxes = new BoundingBoxTree(); private staticBoundingBoxes = new BoundingBoxTree();
private dynamicBoundingBoxes = new BoundingBoxList(); private dynamicBoundingBoxes = new BoundingBoxList();
public addStaticBoundingBox(boundingBox: ImmutableBoundingBox) { public addStaticBoundingBox(boundingBox: ImmutableBoundingBox) {
@ -24,6 +23,62 @@ export class Physics {
this.dynamicBoundingBoxes.insert(boundingBox); this.dynamicBoundingBoxes.insert(boundingBox);
} }
public tryMovingDynamicCircle(
circle: BoundingCircle,
delta: vec2,
invocationCount = 0
) {
circle.center = vec2.add(circle.center, circle.center, delta);
const intersecting = this.findIntersecting(circle.boundingBox).filter(
(b) =>
b.owner !== circle.owner && circle.areIntersecting(b.owner) && b.owner.canCollide
);
const pointCount = 16;
const points = circle.getPerimeterPoints(pointCount);
const distancesOfPoints = points.map((point) => ({
point,
distances: intersecting.map((i) => i.owner.distance(point)).sort((a, b) => a - b),
}));
const distancesOfIntersectingPoints = distancesOfPoints.filter(
(d) => d.distances[0] > 0
);
if (distancesOfIntersectingPoints.length === 0) {
return;
}
const distanceToLeastIntersectingForEachPoint = distancesOfIntersectingPoints.map(
(pointDistances) => ({
point: pointDistances.point,
distance: pointDistances.distances[0],
})
);
const deltas = distanceToLeastIntersectingForEachPoint.map((pointDistance) => {
vec2.subtract(pointDistance.point, circle.center, pointDistance.point);
vec2.normalize(pointDistance.point, pointDistance.point);
vec2.scale(pointDistance.point, pointDistance.point, pointDistance.distance);
return pointDistance.point;
});
const sumDelta = vec2.fromValues(0, 0);
deltas.forEach((d) => vec2.add(sumDelta, sumDelta, d));
vec2.scale(sumDelta, sumDelta, 1 / deltas.length);
if (invocationCount > 10) {
return;
}
this.tryMovingDynamicCircle(circle, sumDelta, ++invocationCount);
}
public findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase> { public findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase> {
return [ return [
...this.staticBoundingBoxes.findIntersecting(box), ...this.staticBoundingBoxes.findIntersecting(box),

View file

@ -0,0 +1,7 @@
import { PhysicalObject } from './physical-object';
export abstract class StaticPhysicalObject extends PhysicalObject {
protected addToPhysics() {
super.addToPhysics(false);
}
}

View file

@ -0,0 +1,3 @@
export const settings = {
lightCutoffDistance: 600,
};

View file

@ -1,12 +0,0 @@
import { vec2 } from 'gl-matrix';
import { GameObject } from '../objects/game-object';
import { BoundingBox } from './bounding-box';
export interface IShape {
readonly boundingBox: BoundingBox;
readonly gameObject?: GameObject;
minDistance(target: vec2): number;
normal(from: vec2): vec2;
clone(): IShape;
}

View file

@ -1,110 +1,89 @@
import { mat2d, vec2 } from 'gl-matrix'; import { mat2d, vec2 } from 'gl-matrix';
import { Circle, Drawable, DrawableDescriptor } from 'sdf-2d'; import { Drawable, DrawableDescriptor } from 'sdf-2d';
import { GameObject } from '../../objects/game-object'; import { BoundingCircle } from '../../physics/bounds/bounding-circle';
import { BoundingBox } from '../bounding-box';
import { IShape } from '../i-shape';
import { CircleShape } from './circle-shape';
export class BlobShape extends Drawable implements IShape { export class BlobShape extends Drawable {
public static descriptor: DrawableDescriptor = { public static descriptor: DrawableDescriptor = {
sdf: { sdf: {
shader: ` shader: `
uniform struct { uniform vec2 headCenters[BLOB_COUNT];
vec2 headCenter; uniform vec2 leftFootCenters[BLOB_COUNT];
vec2 leftFootCenter; uniform vec2 rightFootCenters[BLOB_COUNT];
vec2 rightFootCenter; uniform float headRadii[BLOB_COUNT];
float headRadius; uniform float footRadii[BLOB_COUNT];
float footRadius; //uniform float ks[BLOB_COUNT];
float k;
}[BLOB_COUNT] blobs;
float smoothMin(float a, float b) float smoothMin(float a, float b)
{ {
const float k = 80.0; const float k = 80.0;
float res = exp2( -k*a ) + exp2( -k*b ); float res = exp2( -k*a ) + exp2( -k*b );
return -log2( res )/k; return -log2( res )/k;
}
float circleDistance(vec2 circleCenter, float radius, vec2 target) {
return distance(target, circleCenter) - radius;
}
float blobMinDistance(vec2 target, out float colorIndex) {
float minDistance = 1000.0;
colorIndex = 3.0;
for (int i = 0; i < BLOB_COUNT; i++) {
float headDistance = circleDistance(headCenters[i], headRadii[i], target);
float leftFootDistance = circleDistance(leftFootCenters[i], footRadii[i], target);
float rightFootDistance = circleDistance(rightFootCenters[i], footRadii[i], target);
float res = min(
smoothMin(headDistance, leftFootDistance),
smoothMin(headDistance, rightFootDistance)
);
minDistance = min(minDistance, res);
} }
float circleDistance(vec2 circleCenter, float radius) { return minDistance;
return distance(position, circleCenter) - radius; }
} `,
void blobMinDistance(inout float minDistance, inout float color) {
for (int i = 0; i < BLOB_COUNT; i++) {
float headDistance = circleDistance(blobs[i].headCenter, blobs[i].headRadius);
float leftFootDistance = circleDistance(blobs[i].leftFootCenter, blobs[i].footRadius);
float rightFootDistance = circleDistance(blobs[i].rightFootCenter, blobs[i].footRadius);
float res = min(
smoothMin(headDistance, leftFootDistance),
smoothMin(headDistance, rightFootDistance)
);
minDistance = min(minDistance, res);
color = mix(1.0, color, step(distanceNdcPixelSize + SURFACE_OFFSET, res));
}
}
`,
distanceFunctionName: 'blobMinDistance', distanceFunctionName: 'blobMinDistance',
}, },
uniformName: 'blobs', propertyUniformMapping: {
footRadius: 'footRadii',
headRadius: 'headRadii',
rightFootCenter: 'rightFootCenters',
leftFootCenter: 'leftFootCenters',
headCenter: 'headCenters',
},
uniformCountMacroName: 'BLOB_COUNT', uniformCountMacroName: 'BLOB_COUNT',
shaderCombinationSteps: [1], shaderCombinationSteps: [0, 1, 10],
empty: new BlobShape(vec2.fromValues(0, 0)), empty: new BlobShape(),
}; };
protected head: BoundingCircle;
protected leftFoot: BoundingCircle;
protected rightFoot: BoundingCircle;
public readonly boundingCircleRadius = 100; public constructor() {
protected readonly headRadius = 40;
protected readonly footRadius = 15;
private readonly headOffset = vec2.fromValues(0, -15);
private readonly leftFootOffset = vec2.fromValues(-12, -60);
private readonly rightFootOffset = vec2.fromValues(12, -60);
public readonly isInverted = false;
protected boundingCircle = new CircleShape(vec2.create(), this.boundingCircleRadius);
protected head = new Circle(vec2.create(), this.headRadius);
protected leftFoot = new Circle(vec2.create(), this.footRadius);
protected rightFoot = new Circle(vec2.create(), this.footRadius);
public constructor(center: vec2, public readonly gameObject: GameObject = null) {
super(); super();
this.position = center;
const circle = new BoundingCircle(null, vec2.create(), 200);
this.setCircles([circle, circle, circle]);
} }
public set position(value: vec2) { public setCircles([head, leftFoot, rightFoot]: [
vec2.copy(this.boundingCircle.center, value); BoundingCircle,
vec2.add(this.head.center, value, this.headOffset); BoundingCircle,
vec2.add(this.leftFoot.center, value, this.leftFootOffset); BoundingCircle
vec2.add(this.rightFoot.center, value, this.rightFootOffset); ]) {
} this.head = head;
this.leftFoot = leftFoot;
public get center(): vec2 { this.rightFoot = rightFoot;
return this.boundingCircle.center;
}
public get radius(): number {
return this.boundingCircle.radius;
} }
public minDistance(target: vec2): number { public minDistance(target: vec2): number {
return this.boundingCircle.minDistance(target); return Math.min(
} this.head.distance(target),
public normal(from: vec2): vec2 { this.leftFoot.distance(target),
return this.boundingCircle.normal(from); this.rightFoot.distance(target)
} );
public get boundingBox(): BoundingBox {
return this.boundingCircle.boundingBox;
}
public isInside(target: vec2): boolean {
return this.minDistance(target) < 0;
}
public clone(): BlobShape {
return new BlobShape(this.boundingCircle.center, this.gameObject);
} }
protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {
@ -120,8 +99,8 @@ export class BlobShape extends Drawable implements IShape {
this.rightFoot.center, this.rightFoot.center,
transform2d transform2d
), ),
headRadius: this.headRadius * transform1d, headRadius: this.head.radius * transform1d,
footRadius: this.footRadius * transform1d, footRadius: this.leftFoot.radius * transform1d,
}; };
} }
} }

View file

@ -1,12 +1,8 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { Circle } from 'sdf-2d'; import { Circle } from 'sdf-2d';
import { GameObject } from '../../objects/game-object'; import { GameObject } from '../../objects/game-object';
import { BoundingBox } from '../bounding-box';
import { IShape } from '../i-shape';
export class CircleShape extends Circle implements IShape {
public readonly isInverted = false;
export class CircleShape extends Circle {
public constructor( public constructor(
center = vec2.create(), center = vec2.create(),
radius = 0, radius = 0,
@ -15,34 +11,6 @@ export class CircleShape extends Circle implements IShape {
super(center, radius); super(center, radius);
} }
public distance(target: vec2): number {
return vec2.distance(this.center, target) - this.radius;
}
public normal(from: vec2): vec2 {
const diff = vec2.subtract(vec2.create(), from, this.center);
return vec2.normalize(diff, diff);
}
public get boundingBox(): BoundingBox {
return new BoundingBox(
this,
this.center.x - this.radius,
this.center.x + this.radius,
this.center.y - this.radius,
this.center.y + this.radius
);
}
public isInside(target: vec2): boolean {
return this.distance(target) < 0;
}
public areIntersecting(other: CircleShape): boolean {
const distance = vec2.distance(this.center, other.center);
return distance < this.radius + other.radius;
}
public clone(): CircleShape { public clone(): CircleShape {
return new CircleShape(vec2.clone(this.center), this.radius, this.gameObject); return new CircleShape(vec2.clone(this.center), this.radius, this.gameObject);
} }

View file

@ -1,81 +1,17 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { InvertedTunnel } from 'sdf-2d'; import { InvertedTunnel } from 'sdf-2d';
import { clamp01 } from '../../helper/clamp';
import { rotate90Deg } from '../../helper/rotate-90-deg';
import { GameObject } from '../../objects/game-object';
import { BoundingBox } from '../bounding-box';
import { IShape } from '../i-shape';
export class TunnelShape extends InvertedTunnel implements IShape { export class TunnelShape extends InvertedTunnel {
constructor( constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) {
readonly from: vec2,
readonly to: vec2,
readonly fromRadius: number,
readonly toRadius: number,
public readonly gameObject: GameObject = null
) {
super(from, to, fromRadius, toRadius); super(from, to, fromRadius, toRadius);
} }
public get boundingBox(): BoundingBox {
const xMin = Math.min(this.from.x - this.fromRadius, this.to.x - this.toRadius);
const yMin = Math.min(this.from.y - this.fromRadius, this.to.y - this.toRadius);
const xMax = Math.max(this.from.x + this.fromRadius, this.to.x + this.toRadius);
const yMax = Math.max(this.from.y + this.fromRadius, this.to.y + this.toRadius);
return new BoundingBox(this, xMin, xMax, yMin, yMax);
}
public normal(target: vec2): vec2 {
const targetFromDelta = vec2.subtract(vec2.create(), target, this.from);
const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from);
const h = clamp01(
vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta)
);
let diff = vec2.create();
if (h == 1) {
vec2.subtract(diff, target, this.to);
} else if (h == 0) {
vec2.subtract(diff, target, this.from);
} else {
const side = Math.sign(
toFromDelta.x * targetFromDelta.y - toFromDelta.y * targetFromDelta.x
);
const normal = rotate90Deg(toFromDelta);
vec2.normalize(normal, normal);
const translatedFrom = vec2.add(
vec2.create(),
this.from,
vec2.scale(vec2.create(), normal, side * this.fromRadius)
);
const translatedTo = vec2.add(
vec2.create(),
this.to,
vec2.scale(vec2.create(), normal, side * this.toRadius)
);
diff = rotate90Deg(vec2.subtract(vec2.create(), translatedTo, translatedFrom));
vec2.scale(diff, diff, side);
}
return vec2.normalize(diff, diff);
}
public clone(): TunnelShape { public clone(): TunnelShape {
return new TunnelShape( return new TunnelShape(
vec2.clone(this.from), vec2.clone(this.from),
vec2.clone(this.to), vec2.clone(this.to),
this.fromRadius, this.fromRadius,
this.toRadius, this.toRadius
this.gameObject
); );
} }
} }