Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guiding vectors #140

Merged
merged 9 commits into from
Aug 19, 2018
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ src/**/*.js
src/**/*.js.map
tests/**/*.js
tests/**/*.js.map

*.swp
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vim 👎 😂

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,19 @@
"lint": "tslint -t stylish --project \"tsconfig.json\"",
"test": "npm run test-only",
"test-only": "jest --coverage",
"watch": "tsc -w -p tsconfig.build.json"
"watch": "tsc -w -p tsconfig.build.json",
"webpack": "webpack"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this because I had been doing some development in the graphics lab, which has npm but not yarn. Yarn automatically translates rules like this, but npm doesn't.

},
"author": "",
"license": "MIT",
"dependencies": {
"@types/bezier-js": "^0.0.7",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How production ready is bezier-js? Are there any concerns about using this library?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems pretty legit, the things documented here http://pomax.github.io/bezierjs/ don't seem to have significant active issues (issues are mostly feature requests)

"@types/gl-matrix": "^2.4.0",
"@types/jest": "~22.2.2",
"@types/lodash": "^4.14.107",
"@types/node": "~8.10.0",
"babel-polyfill": "^6.26.0",
"bezier-js": "^2.2.14",
"gl-matrix": "^2.5.1",
"lodash": "^4.17.10",
"regl": "regl-project/regl",
Expand Down
195 changes: 11 additions & 184 deletions src/armature/CostFunction.ts
Original file line number Diff line number Diff line change
@@ -1,193 +1,20 @@
import { coord } from '../calder';
import { AABB } from '../geometry/BakedGeometry';
import { vec3From4 } from '../math/utils';
import { worldSpaceAABB } from '../utils/aabb';
import { Mapper } from '../utils/mapper';
import { CostFn, GeneratorInstance, SpawnPoint } from './Generator';
import { FillVolume } from './FillVolume';
import { Forces, ForcePoint } from './Forces';
import { GuidingVectors } from './GuidingVectors';
import { Model } from './Model';
import { GeometryNode, Node } from './Node';

import { vec3, vec4 } from 'gl-matrix';
import { range } from 'lodash';

export type ForcePoint = {
point: coord;
influence: number;
};

type Grid = { [key: string]: true };
import 'bezier-js';

export namespace CostFunction {
/**
* Use points with positive and negative influence to control generation.
*
* @param {ForcePoint[]} points The points of influence, where negative influence means the
* point reduces the overall cost when nodes get close.
* @returns {CostFn} The resulting cost function.
*/
export function forces(forcePoints: ForcePoint[]): CostFn {
const vectors = forcePoints.map((forcePoint: ForcePoint) => {
return {
vector: Mapper.coordToVector(forcePoint.point),
influence: forcePoint.influence
};
});

return (instance: GeneratorInstance, added: Node[]) => {
// Out of the added nodes, just get the geometry nodes
const addedGeometry: GeometryNode[] = [];
added.forEach((n: Node) =>
n.geometryCallback((node: GeometryNode) => {
addedGeometry.push(node);
})
);

let totalCost = instance.getCost().realCost;

// For each added shape and each influence point, add the resulting cost to the
// instance's existing cost.
addedGeometry.forEach((node: GeometryNode) => {
const localToGlobalTransform = node.localToGlobalTransform();
const globalPosition = vec3From4(
vec4.transformMat4(
vec4.create(),
vec4.fromValues(0, 0, 0, 1),
localToGlobalTransform
)
);

vectors.forEach((point: { vector: vec3; influence: number }) => {
// Add cost relative to the point's influence, and inversely proportional
// to the distance to the point
totalCost +=
point.influence /
vec3.length(vec3.sub(vec3.create(), point.vector, globalPosition));
});
});

return { realCost: totalCost, heuristicCost: 0 };
};
export function forces(forcePoints: ForcePoint[]): Forces {
return new Forces(forcePoints);
}

/**
* Creates a cost function based on how much of a target volume a shape fills.
*
* @param {Model} targetModel The model whose shape we try to fill.
* @param {number} cellSize How big each cell in the volume grid should be.
* @returns {CostFn} The resulting cost function.
*/
// tslint:disable-next-line:max-func-body-length
export function fillVolume(targetModel: Model, cellSize: number): CostFn {
const gridCache = new Map<Node, Grid>();

// A grid uses a string as a key because otherwise it would use object equality on points,
// which we don't want. This function makes a string key from a point.
const makeKey = (point: vec4) => {
const keyX = Math.floor(point[0] / cellSize);
const keyY = Math.floor(point[1] / cellSize);
const keyZ = Math.floor(point[2] / cellSize);

return `${keyX},${keyY},${keyZ}`;
};

// Returns the points that are in a world-space AABB.
const pointsInAABB = (aabb: AABB) => {
const points: string[] = [];
const point = vec4.fromValues(0, 0, 0, 1);
if (isNaN(vec4.squaredLength(aabb.min)) || isNaN(vec4.squaredLength(aabb.max))) {
return [];
}

// Step through x, y, and z from min to max, adding each step to the
// `points` array
range(Math.floor(aabb.min[0]), Math.ceil(aabb.max[0]), cellSize).forEach(
(x: number) => {
range(Math.floor(aabb.min[1]), Math.ceil(aabb.max[1]), cellSize).forEach(
(y: number) => {
range(
Math.floor(aabb.min[2]),
Math.ceil(aabb.max[2]),
cellSize
).forEach((z: number) => {
point[0] = x;
point[1] = y;
point[2] = z;
points.push(makeKey(point));
});
}
);
}
);

return points;
};

const addAABBToGrid = (aabb: AABB, grid: Grid, onAdded: (added: string) => void) => {
pointsInAABB(aabb).forEach((point: string) => {
if (!grid[point]) {
grid[point] = true;

onAdded(point);
}
});
};

// For each node in the target model, find its bounding box
const targetCoords: Grid = {};
targetModel.nodes.forEach((n: Node) =>
n.geometryCallback((node: GeometryNode) => {
pointsInAABB(worldSpaceAABB(node, node.geometry.aabb)).forEach(
(point: string) => (targetCoords[point] = true)
);
})
);

return (instance: GeneratorInstance, added: Node[]) => {
// We will cache grids based on the last node that was added to a model. Since nodes are
// added to the end of a model's node list, if n nodes are new in the current model
// compared to its parent, then the parent grid will be indexed by the nth-from-last
// node in the current model's node list.
const parentGrid = gridCache.get(
instance.getModel().nodes[instance.getModel().nodes.length - 1 - added.length]
);

// If the parent grid does exist, we want to start from the same grid as the
// parent, and then add to it
const grid = parentGrid === undefined ? {} : { ...parentGrid };

let incrementalCost = 0;
let heuristicCost = 0;

// For each point in the new added geometry, see if it fills a point in the target shape
// that wasn't previously filled, and make the current model cost less accordingly
added.forEach((n: Node) =>
n.geometryCallback((node: GeometryNode) =>
addAABBToGrid(
worldSpaceAABB(node, node.geometry.aabb),
grid,
(point: string) =>
// If this point was in the target region, reduce the cost
(incrementalCost +=
cellSize * cellSize * cellSize * (targetCoords[point] ? -1 : 1))
)
)
);

gridCache.set(instance.getModel().latest(), grid);

const heuristicGrid = { ...grid };
instance.getSpawnPoints().forEach((spawnPoint: SpawnPoint) => {
const aabb = instance.generator.getExpectedRuleVolume(spawnPoint.component);
addAABBToGrid(
worldSpaceAABB(spawnPoint.at.node, aabb),
heuristicGrid,
(point: string) =>
(heuristicCost +=
cellSize * cellSize * cellSize * (targetCoords[point] ? -1 : 1))
);
});
export function fillVolume(targetModel: Model, cellSize: number): FillVolume {
return new FillVolume(targetModel, cellSize);
}

return { realCost: instance.getCost().realCost + incrementalCost, heuristicCost };
};
export function guidingVectors(curves: BezierJs.Bezier[]): GuidingVectors {
return new GuidingVectors(curves);
}
}
143 changes: 143 additions & 0 deletions src/armature/FillVolume.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { AABB } from '../geometry/BakedGeometry';
import { worldSpaceAABB } from '../utils/aabb';
import { Cost, GeneratorInstance, SpawnPoint } from './Generator';
import { Model } from './Model';
import { GeometryNode, Node } from './Node';

import { vec4 } from 'gl-matrix';
import { range } from 'lodash';

type Grid = { [key: string]: true };

/**
* Creates a cost function based on how much of a target volume a shape fills.
*/
export class FillVolume {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have an abstract base class or interface for Cost, with methods getCost here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you have the CostFn interface, could you please specify that this class is implementing it?

private gridCache = new Map<Node, Grid>();
private cellSize: number;
private targetCoords: Grid = {};

/**
* @param {Model} targetModel The model whose shape we try to fill.
* @param {number} cellSize How big each cell in the volume grid should be.
* @returns {CostFn} The resulting cost function.
*/
constructor(targetModel: Model, cellSize: number) {
this.cellSize = cellSize;

// For each node in the target model, find its bounding box
targetModel.nodes.forEach((n: Node) =>
n.geometryCallback((node: GeometryNode) => {
this.pointsInAABB(worldSpaceAABB(node, node.geometry.aabb)).forEach(
(point: string) => (this.targetCoords[point] = true)
);
})
);
}

// Returns the points that are in a world-space AABB.
private pointsInAABB(aabb: AABB): string[] {
const points: string[] = [];
const point = vec4.fromValues(0, 0, 0, 1);
if (isNaN(vec4.squaredLength(aabb.min)) || isNaN(vec4.squaredLength(aabb.max))) {
return [];
}

// Step through x, y, and z from min to max, adding each step to the
// `points` array
range(Math.floor(aabb.min[0]), Math.ceil(aabb.max[0]), this.cellSize).forEach(
(x: number) => {
range(Math.floor(aabb.min[1]), Math.ceil(aabb.max[1]), this.cellSize).forEach(
(y: number) => {
range(
Math.floor(aabb.min[2]),
Math.ceil(aabb.max[2]),
this.cellSize
).forEach((z: number) => {
point[0] = x;
point[1] = y;
point[2] = z;
points.push(this.makeKey(point));
});
}
);
}
);

return points;
}

private addAABBToGrid(aabb: AABB, grid: Grid, onAdded: (added: string) => void) {
this.pointsInAABB(aabb).forEach((point: string) => {
if (!grid[point]) {
grid[point] = true;

onAdded(point);
}
});
}

// A grid uses a string as a key because otherwise it would use object equality on points,
// which we don't want. This function makes a string key from a point.
private makeKey(point: vec4): string {
const keyX = Math.floor(point[0] / this.cellSize);
const keyY = Math.floor(point[1] / this.cellSize);
const keyZ = Math.floor(point[2] / this.cellSize);

return `${keyX},${keyY},${keyZ}`;
}

public getCost(instance: GeneratorInstance, added: Node[]): Cost {
// We will cache grids based on the last node that was added to a model. Since nodes are
// added to the end of a model's node list, if n nodes are new in the current model
// compared to its parent, then the parent grid will be indexed by the nth-from-last
// node in the current model's node list.
const parentGrid = this.gridCache.get(
instance.getModel().nodes[instance.getModel().nodes.length - 1 - added.length]
);

// If the parent grid does exist, we want to start from the same grid as the
// parent, and then add to it
const grid = parentGrid === undefined ? {} : { ...parentGrid };

let incrementalCost = 0;
let heuristicCost = 0;

// For each point in the new added geometry, see if it fills a point in the target shape
// that wasn't previously filled, and make the current model cost less accordingly
added.forEach((n: Node) =>
n.geometryCallback((node: GeometryNode) =>
this.addAABBToGrid(
worldSpaceAABB(node, node.geometry.aabb),
grid,
(point: string) =>
// If this point was in the target region, reduce the cost
(incrementalCost +=
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the autoformatter's doing? 🤕 lol

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep :')

this.cellSize *
this.cellSize *
this.cellSize *
(this.targetCoords[point] ? -1 : 1))
)
)
);

this.gridCache.set(instance.getModel().latest(), grid);

const heuristicGrid = { ...grid };
instance.getSpawnPoints().forEach((spawnPoint: SpawnPoint) => {
const aabb = instance.generator.getExpectedRuleVolume(spawnPoint.component);
this.addAABBToGrid(
worldSpaceAABB(spawnPoint.at.node, aabb),
heuristicGrid,
(point: string) =>
(heuristicCost +=
this.cellSize *
this.cellSize *
this.cellSize *
(this.targetCoords[point] ? -1 : 1))
);
});

return { realCost: instance.getCost().realCost + incrementalCost, heuristicCost };
}
}