pixi-cull
Version:
a library to visibly cull objects designed to work with pixi.js
233 lines (216 loc) • 7.89 kB
text/typescript
import { DisplayObjectWithCulling, AABB } from './types'
export interface SimpleOptions {
visible?: string
dirtyTest?: boolean
}
const defaultSimpleOptions = {
visible: 'visible',
dirtyTest: false,
}
export interface SimpleStats {
total: number
visible: number
culled: number
}
type DisplayObjectWithCullingArray = DisplayObjectWithCulling[] & { staticObject?: boolean }
export class Simple {
public options: SimpleOptions
public dirtyTest: boolean
protected lists: DisplayObjectWithCullingArray[]
/**
* Creates a simple cull
* Note, options.dirtyTest defaults to false. Set to true for much better performance--this requires
* additional work to ensure displayObject.dirty is set when objects change)
*
* @param {object} [options]
* @param {string} [options.dirtyTest=false] - only update the AABB box for objects with object[options.dirtyTest]=true; this has a HUGE impact on performance
*/
constructor(options: SimpleOptions = {}) {
options = { ...defaultSimpleOptions, ...options }
this.dirtyTest = typeof options.dirtyTest !== 'undefined' ? options.dirtyTest : true
this.lists = [[]]
}
/**
* add an array of objects to be culled, eg: `simple.addList(container.children)`
* @param {Array} array
* @param {boolean} [staticObject] - set to true if the object's position/size does not change
* @return {Array} array
*/
addList(array: DisplayObjectWithCullingArray, staticObject?: boolean): object[] {
this.lists.push(array)
if (staticObject) {
array.staticObject = true
}
const length = array.length
for (let i = 0; i < length; i++) {
this.updateObject(array[i])
}
return array
}
/**
* remove an array added by addList()
* @param {Array} array
* @return {Array} array
*/
removeList(array: DisplayObjectWithCullingArray): DisplayObjectWithCullingArray {
const index = this.lists.indexOf(array)
if(index === -1){
return array
}
this.lists.splice(index, 1)
return array
}
/**
* add an object to be culled
* NOTE: for implementation, add and remove uses this.lists[0]
*
* @param {DisplayObjectWithCulling} object
* @param {boolean} [staticObject] - set to true if the object's position/size does not change
* @return {DisplayObjectWithCulling} object
*/
add(object: DisplayObjectWithCulling, staticObject?: boolean): DisplayObjectWithCulling {
if (staticObject) {
object.staticObject = true
}
if (this.dirtyTest || staticObject) {
this.updateObject(object)
}
this.lists[0].push(object)
return object
}
/**
* remove an object added by add()
* NOTE: for implementation, add and remove uses this.lists[0]
*
* @param {DisplayObjectWithCulling} object
* @return {DisplayObjectWithCulling} object
*/
remove(object: DisplayObjectWithCulling): DisplayObjectWithCulling {
const index = this.lists[0].indexOf(object)
if(index === -1){
return object
}
this.lists[0].splice(index, 1)
return object
}
/**
* cull the items in the list by changing the object.visible
* @param {AABB} bounds
* @param {boolean} [skipUpdate] - skip updating the AABB bounding box of all objects
*/
cull(bounds: AABB, skipUpdate?: boolean) {
if (!skipUpdate) {
this.updateObjects()
}
for (const list of this.lists) {
const length = list.length
for (let i = 0; i < length; i++) {
const object = list[i]
const box = object.AABB
object.visible =
box.x + box.width > bounds.x && box.x < bounds.x + bounds.width &&
box.y + box.height > bounds.y && box.y < bounds.y + bounds.height
}
}
}
/**
* update the AABB for all objects
* automatically called from update() when calculatePIXI=true and skipUpdate=false
*/
updateObjects() {
if (this.dirtyTest) {
for (const list of this.lists) {
if (!list.staticObject) {
const length = list.length
for (let i = 0; i < length; i++) {
const object = list[i]
if (!object.staticObject && object.dirty) {
this.updateObject(object)
object.dirty = false
}
}
}
}
} else {
for (const list of this.lists) {
if (!list.staticObject) {
const length = list.length
for (let i = 0; i < length; i++) {
const object = list[i]
if (!object.staticObject) {
this.updateObject(object)
}
}
}
}
}
}
/**
* update the has of an object
* automatically called from updateObjects()
* @param {DisplayObjectWithCulling} object
*/
updateObject(object: DisplayObjectWithCulling) {
const box = object.getLocalBounds()
object.AABB = object.AABB || { x: 0, y: 0, width: 0, height: 0 }
object.AABB.x = object.x + (box.x - object.pivot.x) * Math.abs(object.scale.x)
object.AABB.y = object.y + (box.y - object.pivot.y) * Math.abs(object.scale.y)
object.AABB.width = box.width * Math.abs(object.scale.x)
object.AABB.height = box.height * Math.abs(object.scale.y)
}
/**
* returns an array of objects contained within bounding box
* @param {AABB} bounds - bounding box to search
* @return {DisplayObjectWithCulling[]} - search results
*/
query(bounds: AABB): DisplayObjectWithCulling[] {
let results = []
for (let list of this.lists) {
for (let object of list) {
const box = object.AABB
if (box &&
box.x + box.width > bounds.x && box.x - box.width < bounds.x + bounds.width &&
box.y + box.height > bounds.y && box.y - box.height < bounds.y + bounds.height) {
results.push(object)
}
}
}
return results
}
/**
* iterates through objects contained within bounding box
* stops iterating if the callback returns true
* @param {AABB} bounds - bounding box to search
* @param {function} callback
* @return {boolean} - true if callback returned early
*/
queryCallback(bounds: AABB, callback: (object: DisplayObjectWithCulling) => boolean): boolean {
for (let list of this.lists) {
for (let object of list) {
const box = object.AABB
if (box &&
box.x + box.width > bounds.x && box.x - box.width < bounds.x + bounds.width &&
box.y + box.height > bounds.y && box.y - box.height < bounds.y + bounds.height) {
if (callback(object)) {
return true
}
}
}
}
return false
}
/**
* get stats (only updated after update() is called)
* @return {SimpleStats}
*/
stats(): SimpleStats {
let visible = 0, count = 0
for (let list of this.lists) {
list.forEach(object => {
visible += object.visible ? 1 : 0
count++
})
}
return { total: count, visible, culled: count - visible }
}
}