-
Notifications
You must be signed in to change notification settings - Fork 18
Nodes
Nodes are the basic building block of the Scene in Tetra3D. Nodes can serve a spatial purpose (as children inherit their parents' spatial transforms (which are 4x4 matrices that tell the 3D renderer where and how an object is positioned)) and a hierarchical purpose (informing us as to which nodes are related through parenthood). In this way, Tetra3d's node system is similar to how Godot does its node-based scenegraph.
There are, currently, a few different kinds of Nodes in Tetra3D:
- Model, which is an individual, visual instance of a Mesh. This places a 3D model in the scene at a specific location.
- Camera, which is how the scene is viewed. They aren't visible in the game world (understandably), and do not need to be in the scene graph to be used (since their presence in the graph wouldn't influence what they can see, unless they are themselves children of another Node.
- BoundingObjects, which is a category of Node designed for collision / intersection testing. There are four BoundingObject Nodes:
BoundingAABB
,BoundingSphere
,BoundingCapsule
, andBoundingTriangles
. - Lights, which is a category of Node specifically for lighting objects. These include
PointLights
,DirectionalLights
,CubeLights
, andAmbientLights
. - Paths, which is a Node specifically created to allow an object to follow a movement path in-game.
- Node, which is essentially an empty node object, and is the "base" implementation of Nodes. An empty in a GLTF file, when imported, becomes a Node.
- Grids, which is a network of points and connections between these points. You can use grids to pathfind from one point to another, as an example.
Internally, tetra3d.INode
is an interface that defines functions Nodes need to have (position, scaling, rotation, parenting, etc), while actual node objects implement this interface either directly or by embedding Node
:
player := scene.Root.Get("level1/player") // Just an INode
playerModel := player.(*tetra3d.Model) // We know it's a Model, specifically, so we can cast it
playerModel.Color.Set(1, 0, 0, 1) // Set the Model's color
Every Scene has a Root
node, which is the starting point. To see a Model, or to be able to tell that a Node exists within the Scene, you'll need to add it to the Scene's Root node (or one of its children) using the AddChildren()
function:
scene.Root.AddChildren(player)
To remove it from the scene, you can use the root node's RemoveChildren()
function:
scene.Root.RemoveChildren(player)
You can also use the child Node's Unparent()
function for the same result. Simply dropping references to a Node that no longer lies in a Scene's hierarchy should allow Go's garbage collector to collect the object eventually, assuming there's no living references to them. This means that this should be a safe way to "delete" or "destroy" a Node.
Because getting a node from a scene is very common, the tetra3d.Scene
type has functions to quickly get or find a Node:
scene.Get("level/player") // Get a Node by node path exactly...
scene.FindNode("player") // ... Or by searching for the Node by its exact name
Note that you should not embed Tetra3D node structs into other structs. This is because the "owning" struct would not properly inherit the ability to manipulate and be manipulated in a scene hierarchy. See the following for an example:
// Say we wanted to make a custom Camera, and to do this, we tried to embed a
// tetra3d.Camera in our custom struct:
type GameCamera struct {
*tetra3d.Camera
}
func NewGameCamera(t3dCam *tetra3d.Camera) *GameCamera {
return &GameCamera{Camera:t3dCam}
}
func (gc *GameCamera) Update() {}
// Now we can make additional functions and make use of tetra3d.Camera's inherent
// functions and properties.
// However, this is a problem because attempting to, say, unparent a Node from the
// *GameCamera wouldn't work, because nothing would be parented to the
// *GameCamera - children are parented to the internal *tetra3d.Camera reference.
If you want to extend a Tetra3D node with additional functionality (like, say, an Update()
function for a game object), make use of the nodes' Data
properties to add customizeable functionality:
type GameCameraHandler struct {
Camera *tetra3d.Camera
}
func NewGameCameraHandler(t3dCam *tetra3d.Camera) *GameCameraHandler {
// Create a new GameCameraHandler component and set
// the camera's data thusly
handler := GameCameraHandler{ Camera : t3dCam }
t3dCam.SetData(handler)
return handler
}
func (gc *GameCameraHandler) Update() {}
// You now can, for example, loop through all Nodes in a Scene's hierarchy
// and call Update() on them if their Data() structures supports it, while retaining
// the original Tetra3D struct types.
Nodes in Tetra3D can be positioned, scaled, and rotated using world (absolute) or local (relative) positioning. The difference is fairly simple - when using world coordinates, you're setting whichever attribute of the Node in absolute, world space. However, when using local coordinates, you're setting the attribute of the Node in relative space, using the properties of the Node's parents, up through the Node's hierarchy.
For example, let's say a Player model was parented to a Level model, and the Level model were at world position {2, 1, 0}. If you called Player.SetWorldPosition(1, 1, 1)
, then the Player would be at that exact final location - {1, 1, 1}. However, if you called Player.SetLocalPosition(1, 1, 1)
, then the Player would be at {1, 1, 1} relative to the Level model, making its final position {3, 2, 1}, instead.
Getting and setting local properties is, overall, faster than doing so with world coordinates.
Nodes can be cloned using Node.Clone()
. This returns a deep copy of the Node, allowing you to freely change and place the copy in another location in the scene hierarchy, or copy a node from a source scene / library to another scene. You can reparent nodes by calling Node.AddChildren(children ...tetra3d.INode)
, Node.RemoveChildren(children ...tetra3d.INode)
or Node.Unparent()
.
Most resources have such a Clone()
function allowing you to clone them easily. For example, Scenes should also be Cloned if you want to edit their contents while being able to return to their original state by cloning them again to "restart" a given scene.
To easily find a node, you have access to a couple of functions - first is Node.Get()
. This allows you to find a Node using its path.
A Node path is a string that identifies a Node's position in the scene hierarchy. Assuming you had a scene laid out as below:
+ Root
\_ + Level1
\_ + HouseA
\_ + HouseB
\_ Player
The Player's node path would be Level1/HouseB/Player
(the Root node is omitted from the path). You can get a Node's path by calling Node.Path()
, and can find a Node using a node path string by using Node.Get(path)
. The Node's path is also visible in Blender when an object is selected in the Tetra3D Object Properties panel.
While you can use Node.Get()
to get a Node using a path, note that Node.Get(path)
is relative to the starting Node. So, for example, assuming you had a reference to the Player's node, player.Get("Level1/HouseB")
wouldn't work.
Node paths also can "go up one component" using the relative parent notation, "..". In the above example, if you were to want to get the HouseA
node from the Player
node, you could do so with player.Get("../../HouseA")
.
The second way to easily find a node is by searching for it. You can search a tree of Nodes for one (or more) specific Nodes using a NodeFilter
. The NodeFilter
is a slice of INodes, indicating a selection of Nodes that fulfill one or more requirements. To get a NodeFilter
, you would start with the top-level node, call Node.SearchTree()
, and then filter out the nodes you want using the NodeFilter
's filtering functions, as follows:
root := gameScene.Root
// Filter out only the Nodes under the root that have the "kid" tag.
kids := root.SearchTree().ByPropNames("kid")
// You can also chain together filtering functions:
trainers := kids.ByType(tetra3d.NodeTypeNode).ByPropNames("trainer")
// Note that node filtering is lazily executed to prevent allocating a slice for each filter function.
// Call a "finishing function" like .INodes() to get the result as a slice:
result := trainers.INodes()
// NodeFilter.ByFunc() allows you to use a function to filter out a Node as well.
newbs := trainers.ByFunc(func(node tetra3d.INode) bool {
tags := node.Tags()
return tags.Get("age").AsInt() <= 10
}
If you don't need to pass the slice of Nodes to anything, but rather simply want to operate on a set of nodes, you can avoid allocating a slice to store the result by using NodeFilter.ForEach()
:
g.Scene.Root.SearchTree().
ByPropNames("hp").
Not(g.Scene.Root.Get("Player")).
ForEach(
func(node tetra3d.INode) {
// Set the "hp" property to 0 of all nodes with the "hp" property except for the Player
node.Properties().Get("hp").Set(0)
},
)
A Node's Properties
object is essentially a map of string keys to values that can hold any type. You can also easily get Nodes using these properties as identifying tags. For example, if you were to make a bomb that explodes and destroys objects around it, you could assign a damageable
property (with whatever value you wanted) to all Nodes that are damageable, and then get all damage-able nodes nearby with the tag:
bomb := scene.Root.Get("Bomb")
damageable := scene.Root.SearchTree().ByPropNames("damageable").ByFunc(func(damageable INode) bool { return bomb.DistanceTo(damageable) < 10 }).INodes() // Returns []INode
Each Node has a Type() function, which returns the Node's NodeType
. A NodeType is just a string, but it represents the Node's type and can be used to determine if a Node "extends" another node type. As an example, you can determine if a node is a point light:
isPoint := point.Type().Is(tetra3d.NodeTypePointLight) // Returns true if point is a point light
NodeType.Is(baseType NodeType)
returns true if the type is more generic, but not if it's more specific or of a different type:
pType := scene.Root.Get("PointLight").Type() // Let's assume "PointLight" is a point light node
fmt.Println(pType.Is(tetra3d.NodeTypePointLight)) // prints "true" - a Point Light is a Point Light (of course)
fmt.Println(pType.Is(tetra3d.NodeTypeLight)) // prints "true" - a Point Light is a kind of light
fmt.Println(pType.Is(tetra3d.NodeTypeNode)) // prints "true" - a Point Light is a kind of node
fmt.Println(pType.Is(tetra3d.NodeTypeDirectionalLight)) // prints "false" - a Point Light is NOT a Directional Light
You can use the type to search for Nodes as well:
lights := scene.Root.SearchTree().ByType(tetra3d.NodeTypePointLight).INodes() // Returns []INode, but specifically consisting of PointLights
Though there are also functions to easily filter out Lights, Models, and BoundingObjects from NodeFilter
:
onlyModels := scene.Root.SearchTree().Models() // Returns a []*Model
The type hierarchy is as follows:
NodeTypeNode
|
|--- NodeTypeModel
|
|--- NodeTypeCamera
|
|--- NodeTypeBoundingObject
| |
| |--- NodeTypeBoundingAABB
| |
| |--- NodeTypeBoundingCapsule
| |
| |--- NodeTypeBoundingTriangles
| |
| |--- NodeTypeBoundingSphere
|
|--- NodeTypeLight
| |
| |--- NodeTypeAmbientLight
| |
| |--- NodeTypePointLight
| |
| |--- NodeTypeDirectionalLight
| |
| |--- NodeTypeCubeLight
|
|--- NodeTypeGrid
|
|--- NodeTypeGridPoint
|
|--- NodeTypePath
If you want to watch a node tree for changes, you can do that with the TreeWatcher utility struct. You create a new TreeWatcher with a reference to the rootNode and a callback to be called when the tree changes, and then call Tree.Update()
every frame. When any node underneath the root node is parented or unparented, the callback function will be called.
type Game struct {
Watch *tetra3d.TreeWatcher
Scene *tetra3d.Scene
}
func (g *Game) Init() {
g.Watch = tetra3d.NewTreeWatcher(g.Scene.Root, func(node tetra3d.INode) {
fmt.Println("The following node has been removed or added to the scene tree:")
fmt.Println(node)
},
)
}
func (g *Game) Update() error {
g.Watch.Update()
return nil
}
You can also use it, of course, on any node, not just a scene's root node.