-
Notifications
You must be signed in to change notification settings - Fork 1
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
Guiding vectors #140
Changes from all commits
c2f892b
2b8d5e1
e8768c0
aebf25a
4593860
6217939
4836fcc
aca2ea7
97e9a57
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,3 +19,5 @@ src/**/*.js | |
src/**/*.js.map | ||
tests/**/*.js | ||
tests/**/*.js.map | ||
|
||
*.swp | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
|
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); | ||
} | ||
} |
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, CostFn, 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 implements CostFn { | ||
private gridCache: Map<Node, Grid> = 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) | ||
); | ||
}) | ||
); | ||
} | ||
|
||
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 += | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this the autoformatter's doing? 🤕 lol There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }; | ||
} | ||
|
||
// 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}`; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vim 👎 😂