BplusTools.SceneTreeModule

A scene-tree implementation, agnostic as to its memory layout. You can embed it in an ECS, or just throw Nodes into a Vector.

BplusTools.SceneTree.NodeType

The data on a node in a scene tree. It's recommended to stick with the ID-based interface described below, rather than holding onto Node instances. Otherwise you could miss a cache update!

If you don't use a Context for your scene graph, then you can omit it from function calls.

Transforms

Note that getting transform data can cause a node's cache to be updated within the owning Context.

  • local_transform(node_id, context=nothing)::Mat4
  • world_transform(node_id, context=nothing)::Mat4
  • world_inverse_transform(node_id, context=nothing)::Mat4

Operations

  • set_parent(node_id, new_parent_id, context=nothing; preserve_space = Spaces.self)
    • You can pass a null ID for the new parent to make the child into a root node.

Iteration

  • siblings(node_id, context, include_self = true)
  • children(node, context)
  • parents(node, context)
  • family(node, context, include_self = true) : uses an efficient depth-first search.
  • family_breadth_first(node, context, include_self = true) : uses a breadth-first search that works well for smaller trees.
  • family_breadth_first_deep(node, context, include_self = true; buffer = NodeID[ ]) : uses a breadth-first search that works better for larger trees.

Utilities

  • try_deref_node(node_id, context)::Optional{TNode}
BplusTools.SceneTree.deref_nodeMethod

Given a node's unique identifier (assumed to be non-null), retrieves the node itself. If you don't need a 'Context' for your node IDs to be dereferenced, you can define an overload with only the ID parameter.

BplusTools.SceneTree.disconnect_parentMethod

Removes the given node from under its parent. Returns a copy of this node (does not update it in the context), but its parent and siblings will be modified. This is part of the implementation for some other functions, and it has some quirks:

  • It won't invalidate the node's cached world-space data like you'd expect.
BplusTools.SceneTree.familyFunction

Iterates over all nodes underneath this one, in Depth-First order. Must take the root node by its ID, not the Node instance.

For Breadth-First order (which is slower), see family_breadth_first.

BplusTools.SceneTree.family_breadth_firstMethod

Iterates over all nodes underneath this one, in Breadth-first order. Must take the root node by its ID, not the Node instance.

This search is slower, but sometimes Breadth-First is useful. By default, a technique is used that has no heap allocations, but scales poorly for large trees. For a heap-allocating technique that works better on large trees, see family_breadth_first_deep.

BplusTools.SceneTree.family_breadth_first_deepMethod

Iterates over all nodes underneath this one, in Breadth-first order. Must take the root node by its ID, not the Node instance.

This search is slower than Depth-First, but sometimes Breadth-First is useful.

In this version, a heap-allocated queue is used to efficiently iterate over large trees. For smaller trees, consider using family_breadth_first, which will not allocate.

BplusTools.SceneTree.invalidate_world_spaceMethod

Invalidates the cached world-space data. May or may not include the rotation; for example if the node moved but didn't rotate, then there's no need to invalidate its world rotation.

Returns a copy of this node (does not update it in the context), but its children will be modified.

BplusTools.SceneTree.local_transformMethod

Gets (or calculates) the local-space transform of this node, and updates the node's cache if necessary. Returns whether the node's cache was updated, and the new version of the node (which you should write back into the Context if the flag is true).

BplusTools.SceneTree.null_node_idMethod

Creates a 'null' node of the given ID type. If the ID type is unsigned-integer, returns 0. If the ID type is signed-integer, returns -1. If the ID type can be Nothing, returns nothing.

BplusTools.SceneTree.set_parentMethod

Changes a node's parent. If the new parent ID is null, the child is turned into a root node.

Preserves either the world-space or local-space transform of this node. Local-space is the default, because preserving world-space has more floating-point error.

BplusTools.SceneTree.update_nodeMethod

Updates a node's value in its original storage space. The Node type is immutable, so the original has to be replaced with a new copy.

Optionally takes the user-defined 'context'.

BplusTools.SceneTree.world_transformMethod

A messy implementation version; you can call it if you really want but consider the other overload.

Gets (or calculates) the world-space transform of this node. This call may update cached data in its parent, grandparent, etc.

If this node's own cache needs updating, then this function also returns 'true' along with the updated version of this node (which you should write back into the Context).

BplusTools.FieldsModule

A data representation of continuous math, with multidimensional inputs and outputs, and the ability to get analytical derivatives of the math.

Useful for procedural generation and shader stuff.

For example, to generate an RGBA texture using each pixel's UV coordinate you'd want an AbstractField{2, 4, Float32}.

BplusTools.Fields.ConstantFieldMethod

Creates a constant field matching the output type of another field, with all components set to the given value(s).

BplusTools.Fields.ConversionFieldType

Translates data from one float-type to another. You can't really get different float types within a single field expression, but the full DSL allows you to compose multiple field expresions together.

BplusTools.Fields.GradientFieldType

Samples the gradient of a field, in a given direction. The direction can be an Integer representing the position axis, a vector (AbstractField{NIn, NIn, F}) that is dotted with the gradient, or Nothing if the output should be a Vec with all components flattened together (this is most useful for 1D fields, where the gradient is just like a position input).

BplusTools.Fields.ModFieldType

Floating-point modulo: mod(a, b) == a % b. The behavior with negative numbers matches Julia's % operator.

BplusTools.Fields.MultiFieldType

A sequence of fields. Each one will be sampled into an array and exposed to subsequent fields in the form of a TextureField.

BplusTools.Fields.TextureFieldType

Samples from a 'texture', in UV space (0-1). Pixels are positioned similarly to GPU textures, for example each pixel's center in UV space is (pixel_idx_0_based + 0.5) / texture_size.

BplusTools.Fields.GradientTypeMethod

The type of a field's gradient (i.e. per-component derivative).

For example, a texture-like field (2D input, 3D float output) has a Vec{2, v3f} gradient – along each of the two physical axes, the rate of change of each color channel.

BplusTools.Fields.get_fieldMethod

Gets a field's value at a specific position.

If the field implements prepare_field(), then that prepared data may get passed in. Otherwise, the third parameter will be nothing.

BplusTools.Fields.get_field_gradientMethod

Gets a field's derivative at a specific position, per-component.

Defaults to using finite-differences, a numerical approach.

If the field implements prepare_field(), then that prepared data may get passed in. Otherwise, the third parameter will be nothing.

BplusTools.Fields.linear_sample_axisMethod

Recursively performs linear sampling across a specific axis. Takes as input the interpolant between min and max pixels, the min/max pixel coordinates for all axes up to the current one, and the constant pixel coordinates for all axes past the current one.

BplusTools.Fields.wrap_indexMethod

Applies a TextureField's wrapping to the given pixel index component, returning a value between 1 and range_max_inclusive.

BplusTools.Fields.@fieldMacro

Defines a single Julia field.

Takes 3 or 4 arguments:

  • The input dimensions, as an integer.
  • The number type being used (e.x. Float32).
  • [Optional] An instance of a DslState
  • The field's value (e.x. perlin(pos.yzx * 7))

Sample uses:

# Define a 3D voxel grid, i.e. 3D input to 1D output.
# The value of the grid comes from Perlin noise.
perlin_field = @field 3 Float32 perlin(pos)

# Define an "image" field, a.k.a. 2D pixel coordinates to 3D color (RGB).
# Use half-precision, a.k.a. 16-bit float.
# The previous field is available for reference through a special lookup.
field_lookups = Dict(:my_perlin => Stack{AbstractField}([ perlin_field ]))
array_lookups = Dict{Symbols, Array}()
dsl_state = DslState(field_lookups, array_lookups)
image_field = @field(2, Float16, dsl_state,
    # Convert the perlin noise down to 16-bit float,
    #    then spread its scalar value across all 3 color channels.
    (my_perlin => Float16).xxx
)
BplusTools.Fields.@make_math_fieldMacro

Short-hand for generating an AbstractMathField. Sample syntax:

@make_math_field Add "+" begin
    INPUT_COUNT = 2
    value = input_values[1] + input_values[2]
    gradient = input_gradients[1] + input_gradients[2]
end

In the above example, the new struct is named AddField, and the corresponding DSL function/operator is +.

You construct an instance of AddField by providing it with the input fields (and without any type parameters). Input fields with 1D outputs will be automatically promoted to the dimensionality of the other fields' outputs.

The full set of definitions you can provide is as follows:

  • (Required) INPUT_COUNT = [value] defines how many inputs the node can have. Valid formats:
    • A constant: INPUT_COUNT = 3
    • A range: INPUT_COUNT = 2:2:6
    • A min bound: INPUT_COUNT = 3:∞ (using the char \infty)
    • An explicit set: INPUT_COUNT = { 0, 2, 3 }
  • (Required) value = [expr] computes the value of the field, given the following parameters:
    • field: your field instance.
    • NIn, NOut, F, TFields<:Tuple : The type parameters for your field, including the tuple of its inputs (all of which have the same NIn, NOut, and F types).
    • pos::Vec{NIn, F}: the position within the field being sampled.
    • input_values::NTuple{_, Vec{NOut, F}} : the value of each input field at this position.
      • If you don't need this, you can improve performance by disabling it with VALUE_CALC_ALL_INPUT_VALUES (see below).
    • prep_data::Tuple : the output of prepare_field for each input field.
  • gradient = [expr] computes the gradient of the field, given the following parameters:
    • field: your field instance
    • NIn, NOut, F, TFields<:Tuple : The type parameters for your field, including the tuple of its inputs (all of which have the same NIn, NOut, and F types).
    • pos::Vec{NIn, F}: the position input into the field.
    • input_gradients::NTuple{_, Vec{NIn, Vec{NOut, F}}} : the gradient of each input field at this position.
      • If you don't need this, you can improve performance by disabling it with GRADIENT_CALC_ALL_INPUT_GRADIENTS (see below).
    • input_values::NTuple{_, Vec{NOut, F}} : the value of each input field at this position.
      • Not provided by default; you must enable it with GRADIENT_CALC_ALL_INPUT_VALUES (see below).
    • prep_data::Tuple : the output of prepare_field for each input field.
    If not given, falls back to the default behavior of all fields (numerical solution).
  • VALUE_CALC_ALL_INPUT_VALUES = [true|false]. If true (the default value), then the computation of value has access to a local var, input_values, containing all input fields' values. This can be disabled for performance if your math op doesn't always need every input's value.
  • GRADIENT_CALC_ALL_INPUT_VALUES = [true|false]. If true (default value is false), then the computation of gradient has access to a local var, input_values, containing all input fields' values. This is disabled by default for performance; enable if your math op needs every input's value to compute its own gradient.
  • GRADIENT_CALC_ALL_INPUT_GRADIENTS = [true|false]. If true (the default value), then the computation of gradient has access to a local var, input_gradients, containing all input fields' gradients. This can be disabled for performance if your math op doesn't always need every input's gradient.
BplusTools.Fields.@multi_fieldMacro

Defines a sequence of fields, each one getting sampled into an array/texture and used by subsequent fields, culminating in a final field which can sample from all of the previous ones.

For the syntax of each field, refer to the @field macro.

Sample usage:

@multi_field begin
    # 2D perlin noise, sampled into a 128x128 texture.
    perlin1 = 128 => @field(2, Float32, perlin(pos * 20))

    # 3D perlin noise, sampled into a 32x32x128 texture.
    perlin2 = {32, 32, 128} => @field(3, Float32, perlin(pos * 20 * { 1, 1, 10 }))

    # The output field:
    @field(3, Float32,
        (perlin1{pos.xy}) *
        (perlin2{pos})
    )
end
BplusTools.FileCacherType

Keeps track of file data of a certain type and automatically checks it for changes. Has the following interface:

  • Update it and check for modified files with check_disk_modifications!().
    • Ideally, call this every frame.
    • If at least one file changed, it returns true, and also raises a callback to reload each changed file.
  • Get a file with get_cached_data!(). You can provide both relative and absolute paths.
  • Get the canonical representation of a file path within the cacher with get_cached_path().

You can configure the cacher in a few ways:

  • reload_response(path[, old_data]) -> new_data[, dependent_files] is called when either a file is being loaded for the first time, OR a file has changed and should be reloaded.
    • Returns the new file, and optionally an iterator of 'dependent' files. whose changes will also trigger a reload.
    • If your response throws an error then error_response, mentioned below, is invoked.
  • error_response(path, exception, trace[, old_data]) -> [fallback_data] is called when a file fails to load or re-load.
    • By default it @errors and returns nothing.
    • You can change it to, for example, return a fallback 'error data' object, or the previous version of the object.
  • check_interval_ms is a range of wait times in between checking files for changes. A wait time is randomly chosen for each file, to prevent them from all checking the disk at once.
  • relative_path is the prefix for relative paths.
    • By default, it's the process's current location.
BplusTools.cam_updateMethod

Updates the given camera with the given user input, returning its new state. Also returns the new Settings, which may be changed.

BplusTools.check_disk_modifications!Function

For a variety of objects, this checks all their known files' last-modified timestamp, and returns whether any of them have changed.

BplusTools.get_cached_data!Method

Retrieves the given file data, using a cached version if available.

Returns nothing if the file can't be loaded and no fallback error value was provided to the cacher.

BplusTools.ECS.WorldType

An ordered collection of entities, including accelerated lookups for groups of components

BplusTools.ECS._EntityType

An organized collection of components.

The World type is defined afterwards, so it's hidden through a type parameter. You should refer to this type using the alias Entity.

BplusTools.ECS.add_componentMethod

Any other components that are required by the new component will be added first, if not in the entity already.

BplusTools.ECS.count_componentsMethod

Counts the number of instances of the given component in the given entity.

Note that for UnionAll types the operation is a bit slower and requires a data structure; you pass in a buffer to avoid heap allocations in this case.

BplusTools.ECS.count_componentsMethod

Counts all components in the world of the given type.

For UnionAll types, the operation is a bit slower and requires a data structure; you pass in a buffer to avoid heap allocations in this case.

BplusTools.ECS.create_componentMethod

Creates a new component that will be attached to the given entity.

Any dependent components named in require_components() will already be available, except in recursive cases where multiple components require each other.

By default, invokes the component's constructor with any provided arguments.

BplusTools.ECS.get_componentMethod

In Debug mode, throws an error if there is more than one of the given type of component for the given entity

BplusTools.ECS.get_componentMethod

Gets a singleton component, assumed to be the only one of its kind. Returns its owning entity as well.

BplusTools.ECS.get_componentsMethod

Gets an iterator of all instances of the given component attached to the given entity.

Note that for UnionAll types the operation is a bit slower and requires a data structure; you pass in a buffer to avoid heap allocations in this case.

BplusTools.ECS.get_componentsMethod

Gets an iterator of all instances of the given component in the entire world. Each element is a Tuple{T, Entity}.

Note that for UnionAll types the operation is a bit slower and requires a data structure; you pass in a buffer to avoid heap allocations in this case.

BplusTools.ECS.is_entitysingleton_componentMethod

Gets whether an entity can hold more than one of the given type of component.

If your components inherit from another abstract component type, it's illegal for the abstract type to return a different value than the concrete child types.

BplusTools.ECS.is_worldsingleton_componentMethod

Gets whether a world can have more than one of the given type of component.

If your components inherit from another abstract component type, it's illegal for the abstract type to return a different value than the concrete child types.

BplusTools.ECS.remove_componentMethod

This is allowed even if the component is required by another one. It's up to you to make sure your components either handle that or avoid that!

NOTE: the named keywords are for internal use; do not use them.

BplusTools.ECS.require_componentsMethod

Gets the types of components required by the given component.

If your components inherit from another abstract component type, it's illegal for the abstract type to specify requirements unless the concrete child types also specify them.

If you name an abstract component type as a requirement, make sure to define create_component() for that abstract type (i.e. a default to use if the component doesn't already exist).

BplusTools.ECS.tick_componentMethod

Updates the given component attached to the given entity.

Note that the entity reference is only given for convenience; the component will always have the same Entity owner that it did when it was created.

BplusTools.ECS.@componentMacro

A very covenient way to define a component.

The basic syntax is this:

@component Name [<: Parent] [attributes...] begin
    field1::Float32
    field2

    # By default, a component is constructed by providing each of its fields, in order.
    # Inherited fields come after the new fields.
    # However, you can override this like so:
    function CONSTRUCT(f)
        # All functions within a @component can reference "this", "entity", and "world".
        this.field1 = Float32(f)
        this.field2 = length(world.entities)
        # You must call SUPER to invoke the parent constructor.
        SUPER(3, f+4)
    end
    function DESTRUCT() # Optionally take a Bool for whether the owning entity is being destroyed
        println("Delta entities: ", length(world.entities) - this.field2)
    end

    function TICK()
        this.field1 += world.delta_seconds
    end
    # FINISH_TICK() exists, but is only useful for abstract components (see below).

Attributes are enclosed in braces. Here are the supported ones:

  • {abstract}: this component is an abstract type (see below).
  • {entitySingleton}: only one of this component can exist on an entity.
  • {worldSingleton}: only one of this component can exist in the entire world.
  • {require: a, b, c}: other components must exist on this entity, and will be added if they aren't already

This macro also provides an Object-Oriented architecture, where abstract components can add fields, behavior, "Promises" (abstract functions) and "Configurables" (virtual functions) to their concrete children. Here is a detailed example of an abstract component:

"Some kind of animated motion using float position data. Only one maneuver can run at a time."
@component Maneuver{F<:AbstractFloat} {abstract} {entitySingleton} {require: Position} begin
    pos_component::Position{F}
    duration::F
    progress_normalized::F

    # Because this type has a custom constructor, all child types must have one too.
    # They also must invoke SUPER(...) exactly once, with these arguments.
    function CONSTRUCT(duration_seconds::F)
        this.progress_normalized = 0
        this.duration = duration_seconds
        this.pos_component = get_component(entity, Position)
    end

    # If you want to provide a default instance of this component,
    #    for when it's required but not already existing on an entity,
    #    you can implement this (pretend a child maneuver exists called 'TurningManeuver'):
    DEFAULT() = TurningManeuver(3.5, has_component(entity, PreferLeft) ? -90 : 90)

    # Non-abstract child components must implement this.
    # Abstract child components may choose to implement this for their children.
    # In the latter case, the concrete children can further override the behavior,
    #    and invoke `SUPER()` to get the original parent implementation.
    # If `SUPER()` is called with no arguments,
    #    then the arguments given to the child implementation are automatically forwarded.
    # If a child implements this, then a grand-child implements it with different parameter types,
    #    the child and grand-child can both participate in overload resolution,
    #    meaning children can extend or specialize parent promises.
    @promise finish_maneuver(last_time_step::F)

    # Child components may choose to override this.
    # It must return a bool.
    # It indicates whether to cut off the maneuver early.
    # When overriding this, you can invoke SUPER() to get the implementation of your parent.
    # If `SUPER()` is called with no arguments,
    #    then the arguments given to the child implementation are automatically forwarded.
    # If a child implements this with different parameter types,
    #    the parent and child component can both participate in overload resolution,
    #    meaning children can extend or specialize a parent's configurables.
    @conigurable should_stop()::Bool = false

    # This abstract base type handles the timing for its children.
    # Base class TICK() is called before children's TICK().
    function TICK()
        this.progress_normalized += world.delta_seconds / this.duration
    end
    # After all tick logic is done (including children), check whether the animation is finished.
    # Base class FINISH_TICK() runs after children's FINISH_TICK().
    function FINISH_TICK()
        if this.should_stop()
            remove_component(entity, this)
        elseif this.progress_normalized >= 1
            # Handle any overshoot of the target, then destroy self:
            overshoot_seconds = (@f32(1) - this.progress_normalized) * this.duration
            this.finish_maneuver(-overshoot_seconds)
            remove_component(entity, this)
        end
    end
end

Finally, here is an example of StrafingManeuver, a child of Maneuver that only uses Float32:

@component StrafingManeuver <: Maneuver{Float32} {require: MovementSpeed} begin
    speed_component::MovementSpeed
    dir::v3f
    function CONSTRUCT(duration::Float32, dir::v3f)
        SUPER(duration)
        this.speed_component = get_component(entity, MovementSpeed)
        this.dir = dir
    end

    # Implement the maneuver's interface.
    finish_maneuver(last_time_step::Float32) =
        strafe(this, last_time_step) # Uses a helper function defined below
    should_stop() =
        do_collision_checking(entity) # Some function that checks for this entity colliding with geometry

    TICK() = strafe(this) # Uses a helper function defined below
end

# Helper function for applying the maneuver.
function strafe(s::StrafingManeuver, time_step)
    # In a normal Julia function, things are a bit less convenient.
    entity = s.entity
    world = s.world

    if isnothing(time_step)
        time_step = world.delta_seconds
    end
    s.pos_component.value += s.speed_component.value * time_step * s.dir
end

As a complement to the usual reflection data provided by Julia, you may access component reflection data (such as the declared @promises) through the interface at the top of this file, src/ecs/macros.jl. However note that this is an internal interface which may change in the future.