This is a work in progress and subject to modification and additions.
Written and © Alex "no-lex" Foster, 2020-2021; released under the WTFPL v2.
This text is an expository one, describing the what and some of the why things are implemented as they are in the engine. This document does not attempt to explain how the game is implemented.
Note that this text is written by someone who did not write the actual code for the engine: as a result, it is possible that some of this document is misconstrued from the original intent of the code writers. As those people are not particularly available, this text represents the best guess of the author.
This text is an attempt to describe the engine in such a way that previous Cube-based engines were not. For nearly two decades, the Cube series of engines, including the original Cube as well as Cube 2 and Tesseract, have been among the most technologically advanced first-person shooter engines available to the open source community, but their adoption has been limited by the relative inability to understand the source code due to a lack of comprehensive documentation and often opaque coding styles. This text is an attempt to lay down the architecture of the engine, and provide the skeleton required to understand specific code that is written in the engine.
To fully understand this text, it is expected that readers understand the basics of functional and object-oriented programming paradigms, as well as have a basic knowledge of linear and vector algebra; these mathematical concepts are the core of how 3D engines like Libprimis' are designed. A few rendering techniques, such as global illumination, additionally use more complex mathematics borrowed from linear analysis, including multipole expansions and Fourier series.
To be a functioning project, Libprimis has some standards which make it straightforward to communicate effectively. While some particular standards may be clumsy for particular uses, it is important that the project be consistent, not only for clarity of code but also for documentation and ease of extension.
This file is written to be interpreted by GitHub Flavored Markdown (GFM) and must satisfy the standards laid out therein.
Text should institute a line break after 80 characters.
Chapters use #
; sections use ##
; subsections use ###
. Minor topics within
subsections can use ####
.
Four spaces per indentation, spaces only. Opening brackets get their own new line; this is called "Allman style".
Macros are always fully capitalized and seperated with underscores:
#define MACRO_NAME
#define MACRO_FUNCTION(a, b)
Do not use macros unless there is no other salient way to approach the problem. If a macro is the best way to solve a particular problem, it should be explained with surrounding comments.
Variables are always lowercase:
int varname
Functions are always lowercase:
void functionname()
Enum elements are in PascalCase:
enum
{
Name_ElementOne = 1,
Name_ElementTwo = 2,
Name_ElementThree = 3,
};
Enums also are always expanded with a single element per line. For aesthetics, it is best to place all the equals in the same location (as above). Libprimis uses no named enums besides those inhereted from ENet.
Objects of POD type (Plain Old Data) should be assigned the keyword struct
,
and objects of a complex type should be given the keyword class
. Where
applicable, objects should be separated into public
and private
, with
public
members being declared first.
The accepted conventional variables for loops are the single letter i
,j
,k
,
l
temp vars. Use later loop variables only if the earlier ones are already
present (don't use a loop over j
unless it's inside a loop over i
).
The convention for incrementing loops (counting upwards) is as follows:
for(int i = 0; i < N; ++i)
Change the value passed to N
rather than using a less than or equals sign <=
so as to keep all for() statements clear as to their termination point
immediately.
For decrementing loops (counting downwards):
for(int i = N; --i >= 0;)
For loops over the length of a vector, use
for(int j = 0; j < N.length(); j++) //forward iteration
for(int v = m; --v >= 0;) //reverse iteration
The codebase uses the Allman style; that is, statements are enclosed in brackets on newlines. Case statements are indented one tab past their opening switch statement.
namespace MyNamespace
{
int var1, var2, var3;
int var4 = foo(myfunction()),
var5 = bar(foo()),
var6 = foo(foo() || foo(bar) || foo(baz));
enum
{
Name_ElementOne = 1,
Name_ElementTwo = 2,
Name_ElementThree = 3,
};
struct baz
{
int var1;
char var2;
union
{
char var3[8];
int var4;
};
}
if(foo())
{
int a;
int b;
doStuff;
doMoreStuff;
}
else
{
doStuff;
}
do
{
doStuff;
} while(bar)
while(foo)
{
doStuff;
}
//=====================================================================MACRONAME
#define MACRO_NAME(a,b) stuff(a,b)
int myfunction(int a, int b = 0)
{
for(int i; i < N; ++N)
{
if(bar)
{
switch(n)
{
case 1:
{
doStuff;
}
case 2:
{
doStuff;
}
case 3:
case 4:
case N:
{
doStuff;
}
}
}
}
MACRO_NAME(a,b)
return a;
}
}
#undef MACRONAME
//==============================================================================
template<class Foo>
inline Foo func(Foo var)
{
bar(var);
return(var);
}
Note that template declarations should have the template declaration on its own line.
Control flow statements (if/while/do-while etc.) should get their own line; don't do stuff like
if(foo) for(int i; i < N; ++i) { doStuff; doMoreStuff }
In addition, always delimit statements after control expressions with curly braces, even if there is only one expression:
//do this
if(foo)
{
doStuff;
}
//not this
if(foo)
doStuff;
Ternaries and boolean operators should be spaced out between each element:
foo ? bar : baz
foo >= bar
foo || bar
Arithmetic can be done without spaces, however:
foo+1
3*bar
Operators should remain packed against their parentheses:
if(foo)
{
stuff
}
while(bar)
{
stuff
}
Linux: ~/.imprimis
is the "home" directory by default.
Windows: ~/My Games/Imprimis
is the "home" directory by default.
This game requires libsdl2, libsdl2-mixer, libsdl2-image
, and libsdl2-ttf
to run, including the -dev
versions for those package managers which elect to
seperate them (such as Ubuntu) if compiling the game. As the main way of getting
the game is via Git, the best way to retrieve the assets for the game is by the
command line utility git
.
Libprimis uses some standard units which allow for users to contextualize and orient themselves in the worlds it creates.
Distance is always in the unit of cube units ("cubits"), which is the size of a gridpower 0 cube, when not specified. This distance is equal to an eighth of a meter, 12.5 centimeters, or approximately five inches (to within a couple %).
Octree nodes, in particular, are always positive and the origin is located at the bottom northwest corner of the map; the coordinates count upwards as you move more southerly or easterly from that point.
The engine uses a left-handed coordinate system, which is the opposite chirality from typical 3D coordinate systems used in math and engineering. This means that the positive orientation is clockwise, and cross products follow the left hand rule. This makes the game's coordinates follow the compass orientation well and is perhaps more intuitive for those used to maps where degrees count clockwise, but be aware that the usual identities for right handed systems are mirrored in this coordinate system.
Zero yaw degrees in the engine's coordinate system is facing along the +y axis; the +x axis is at 270 degrees (or to the left side of the y axis, hence the name of the coordinate system); pitch is measured as an altitude where 0 degrees is the horizon, the +z axis is at 90 degrees, and the -z axis is at -90 degrees.
Position coordinates for valid geometry is always positive, as all cubes must be placed NE of the origin, located in the lower SE corner of the map. This should somewhat simplify coordinate calculations, but is mostly there because the root octree node is most easily represented with a corner at (0,0,0) rather than trying to be centered. As a result, larger maps will have their greater extent at larger coordinates; a smaller map will occupy the SE corner of a larger map.
+z +z
| +x |
| / |
| / |______+y
|/_____+y /
/
/
+x
LH Coordinates RH Coordinates
Clockwise Counterclockwise
As a 3D engine is largely defined in terms of vectors and linear algebra, some understanding of these concepts is very helpful in understanding positions and transformations in the engine as well as the rendering machinery that renders images to the screen.
A vector is a set of multiple scalars (standard numbers) which together
represent a multidimensional location in some space. This space does not have to
be positional, as concepts like colors are usually also represented as a vector
(components are R
ed, G
reen, and B
lue). Most vectors in Libprimis are three
dimensional, but second dimensional, fourth dimensional, complex (quaternions)
and dual (dualquats) are also represented.
A particular notational standard which will always be implicit is the difference between speed and velocity. Velocity is a vector, which means it carries a direction along with its size. On the other hand, speed is merely the size of the velocity vector, and does not imply a particular direction.
The complex objects, quaternions and dual quaternions, are not strictly vectors, but exist in a higher dimensional space than standard scalars in a similar way. The structure of the spaces in which these values live is particularly convenient for the constrained values that kinematics problems have, and as such are particularly useful in animation and rotation transformations.
Quaternions have four values, designated x, y, z, w; dual quaternions have similarly their eight values seperated into dual numbers which are each designated x, y, z, w.
Colors which are defined past 0xFFFFFF (hex color for white) are generally
passed as a set of three paramaters R G B
where 1.0 1.0 1.0
is 0xFFFFFF.
These colors tend to have 1.0 1.0 1.0
as the default and are expected to vary
upwards as much as downwards in practice.
The Imprimis project is, at its highest level, organized into four main projects as well as a pair of utilities which are perhaps not considered direct members of the engine.
/--------------------+-----------------------------------------------------------\
| Serverside | Clientside |
+--------------------+-----------------------------------------------------------+
| +--------+ (A) | |
| | Master +---------+------\ |
| | Server | | | |
| +--------+ | v |
| ^ | +--------+ +--------+ +--------+ |
| | +--------+ | | | | | | | |
| +--+ Game |<-+->| | | | | | +--------------+ |
| | | Server | | | | | | | +-->| Window Output| |
| | +--------+ | | | | | | Simple | +--------------+ |
| | | | Game | | Game | | Direct | |
| (B)| +--------+ (C) | Code |(D)| Engine |(E)| Media |(F)+--------------+ |
| +--+ Game |<-+->| |<->| |<->| Layer +-->| Sound Output | |
| | | Server | | | | | | | (SDL) | +--------------+ |
| | +--------+ | | | | | | | |
| | | | | | | | | +--------------+ |
| | +--------+ | | | | | | |<--+ User Input | |
| \--+ Game |<-+->| | | | | | +--------------+ |
| | Server | | | | | | | | |
| +--------+ | +--------+ +--------+ +--------+ |
| | ^ |
| | |(G) |
| | v |
| | +--------+ |
| | | Local | |
| | | Server | |
| | +--------+ |
\--------------------+-----------------------------------------------------------/
- A: Master server provides a list of game servers to the game code via enet.
- B: Game servers register to a master server via enet.
- C: Game servers can be connected to a client's game via enet.
- D: The game engine's behavior is controlled by the game code.
- E: The Simple DirectMedia layer library handles input/output.
- F: Display, sound, and user inputs get handled by SDL.
- G: Local gameplay can be run through a game server hosted clientside.
The components of the system handled by the Libprimis project is the:
- Game engine: core routines needed for the gamecode to run a game
- Game code: an actual game written using the facilities written in the engine
- Game server: a locally or remotely hosted server that manages game clients
- Master server: a service that provides a list of game server names to clients
Libprimis' files are organized essentially into three folders, which carry out
largely different roles in the application. The folder containing the engine
proper, naturally, is called /engine
. There is also a folder for shared
library-like functionality in /shared
while the game code is located in a
folder called /game
.
The engine's core functionality is implemented in the engine
folder. This
folder is itself separated into four main catagories, the scope of which is
briefly described below:
The interface folder contains the source code used for user-interfacing purposes such as the UI system, scripting, sound, and input.
command.cpp
scripting language (Cubescript) implementationconsole.cpp
ingame console supportinput.h/cpp
key/cursor input handlingmenus.cpp
hardcoded UI menu functionalitysound.cpp
sound handling and supporttextedit.h
text box UI functionalityui.cpp
Cubescript user interface functionality
The model
folder contains headers that implement functionality required to
parse different model formats. There are no source code files in this folder,
and this folder is only included by rendermodel.cpp
, located in render
.
animmodel.h/cpp
generic animated model supporthitzone.h
model hitboxesmd5.h
md5 model format supportmodel.h
generic model supportobj.h/cpp
obj (wavefront) model format supportragdoll.h/cpp
skeletal model ragdoll supportskelmodel.h/cpp
generic skeletal model supportvertmodel.h
generic vertex model support
The render
folder contains the core rendering code used to implement the
visual effects in the engine.
aa.h/cpp
antialiasing functionalityao.h/cpp
screenspace ambient occlusion functionalitycsm.h/cpp
cascaded shadow maps (for sunlight)grass.h/cpp
grass rendering on octree geometryhdr.h/cpp
high dynamic range & tonemappingnormal.h/cpp
normal maps & normal map interpolationoctarender.h/cpp
octree geometry renderingradiancehints.h/cpp
radiance hints (global illumination)rendergl.h/cpp
lower-level renderer corerenderlights.cpp
point light & sunlight renderingrendermodel.h/cpp
player, world model renderingrenderparticles.h/cpp
billboard particle renderingrendersky.h/cpp
skybox & other parallax free sky effectsrendertext.h/cpp
ui text renderingrenderva.h/cpp
octree geometry vertex array renderingrenderwindow.h/cpp
window handlingshader.cpp
glsl shader handlingstain.h/cpp
dynamic stain creation and renderingtexture.h/cpp
world geometry texture applicationwater.h/cpp
water material & its fx (screenspace reflection etc.)
The world
folder contains code creating and handling the ingame world,
including entity, player, particle, and physics support.
bih.h/cpp
bounded interval hierarchy (model collision)dynlight.cpp
light entities which move around on the levelexplosion.h
explosion effectslight.h/cpp
light entity supportmaterial.cpp
in-world special materials (water, lava, etc.)mpr.h
minkowski portal refinement (model collision)octa.h/cpp
octree geometry for the worldoctaedit.cpp
octree modification by clientsphysics.cpp
player model modificationraycube.h/cpp
octree geometry queryingworld.h/cpp
world handling and modificationworldio.cpp
world loading and saving
The Libprimis engine, like any other program, is designed for particular underlying hardware and software contexts. The Libprimis engine does not especially target a huge breadth of hardware and operating system contexts, less than even its predecessor engines, as a result of Libprimis' focus on providing good support to the platforms it does target.
Libprimis officially supports only the 64-bit Windows and Linux platforms. While a 64-bit OS is not strictly required to implement the engine, 32-bit OSes have fallen from widespread use and as a result are not part of what the engine explicitly targets.
The macOS operating system is not supported by the engine, for a handful of reasons:
- the macOS system charges developers to distribute programs widely on macOS
- macOS has deprecated OpenGL support, required for the game to run
- macOS is moving away from x86 and commodity CPUs & GPUs
Libprimis currently targets only the x86-64 instruction set, widely used by desktops and laptops. Libprimis does not currently target ARMv8 (used by mobile devices) or POWER architectures which are not widely used for gaming.
Libprimis only supports little-endian bit ordering and as a result lacks the capabilities to run on big-endian systems such as the aformentioned POWER.
For graphics processing, hardware capable of supporting the OpenGL 4.0 standard is required; this includes graphics at least as new as Intel Ivy Bridge, AMD's Northern Islands (HD 5000 series), or Nvidia's Fermi (GTX 400 series). Older hardware support is omitted as few cards of this era are capable of running Libprimis at reasonable frame rates.
The engine does not require the SSSE3 (Supplemental Streaming SIMD Extension 3) extensions required of many games and as a result can run on Phenom era processors without issue.
The Libprimis engine is single-threaded, making multicore CPUs irrelevant to the engine's performance; therefore, any big-core (non Atom or Jaguar based) CPU since around 2010 should be easily adequate to run well-designed levels on Libprimis.
At least two gigabytes of RAM is recommended to run the game, while four or more is easily sufficient to fully utilize the engine.
Nearly all graphics cards that support the OpenGL 4.0 standard should be able to run Libprimis at reasonable speeds, though a card of the RX 460/GTX 950 class is needed to bump graphics up to high levels.
The world is the name for the level that the game is played on, including the octree geometry, materials, and cloud/skyboxes. The world does include the static entities as well which are placed upon it, but those are instead described in the third chapter, which covers entities and actors in more detail.
The world's biggest feature is the octal tree that makes up the primary level geometry. This geometry has the advantage of being easily occludable and simple to modify on the fly, as opposed to models which are placed on the world as static entities.
The Libprimis engine's geometry system is very different than most engines and has different strengths and weaknesses with respect to typical polygon soup engines such as Unreal or the Quake family of engines. Libprimis' octal tree geometry does not record map vertices in terms of typical positon vectors, as the vast majority of 3D rendering software uses, instead opting to use an octal tree format.
Libprimis stores its data in an octal tree, in which each cube of edge length l is divided into eight cubes with edge length l/2. This allows for a level to be efficiently and recursively divided into smaller and smaller pieces. The power of this data structure is that large, faraway objects can occupy relatively fewer nodes in the data tree than objects in the level have.
The gridpower
of a particular octree node (henceforth called simply a "cube")
is an indication of what level it is on the octal tree and therefore also its
size. A gridpower 0 cube is 12.5 cm on edge (~5 inches), a gridpower 1 cube is
25cm (~10 inches) on edge, and a gridpower n cube is 2^(n-3) m on edge.
Level sizes are also defined most conveniently in terms of gridpower; the default map is 2^10 = 2^7m = 128m on edge. Due to limitations of the renderer's z-buffer precision, distances beyond about half a kilometer on edge are not generally recommended.
Cubes in Libprimis, the most basic form of geometry in the game, therefore occupy the octree; instead of vertices in other engines being determined by their 3D vector from the origin, a cube's place in the octal tree determines its location.
As mentioned above, each node can have children defined for them which
themselves can have their own child nodes. The maximum depth of this tree is
equal to the mapsize
: a map of size 2^10 = 1024 cubes (128m square) can have
child nodes up to 10 deep.
^ +z
|
__________
/ 4 / 5 /
/____/____/.
/ 6 / 7 / .
/____/____/ .
. _____.___.
. / 0 /. 1 / +x
. /____/_.__/ ->
. / 2 / 3. /
./____/___./
/ +y
|/
The assignment of child nodes is outlined above. The node with the lowest x,y,z coordinates is assigned as child node #0, and counts upwards to node 7, which is at the largest x,y,z coordinates. A child node might be found in this way within the engine:
level 0 128m worldroot
__________________|_________________
| | | | | | | |
level 1 64m 0 1 2 3 4 5 6 7
__________________|_________________
| | | | | | | |
level 2 32m 0 1 2 3 4 5 6 7
__________________|_________________
| | | | | | | |
level 3 16m 0 1 2 3 4 5 6 7
__________________|_________________
| | | | | | | |
level 4 8m 0 1 2 3 4 5 6 7
^
| (6,5,1,6)
This is an 8*8*8m cube node.
The "gridpower" of a cube is related to the distance down the tree that a cube is, and therefore its size as a power of two. The bottom of the tree is always at gridpower 0, and is located eleven rungs down (0,1,2,3,4,5,6,7,8,9,10) the world octree on a standard power 10 map (128m on edge).
In this view, the ease of discarding smaller nodes is apparent: the cube selected (7,5,1,6) could be discarded easily if it was found that cube (7,5) was occluded, since its "address" includes all the larger cubes that occupy its volume. This is very fast compared to a pile of vectors which need to be individually treated in order to ensure they can be excluded from the render process.
The worldroot, indicated in the octal tree diagram above at the top, is the master cube inside which all geometry fits. Geometry may not leave the area bounded by the worldroot cube, as all geometry is carved out of the worldroot's children nodes (all cubes in a level are child cubes of the worldroot cube).
Because of this, maps always have the following properties:
- Maps are always square. Cube nodes are all sqaure, and thus the largest cube node is as well.
- Maps are of fixed size. The size of the worldroot defines the map's size.
- Maps' range of gridpowers allowed is determined by the size of the worldroot cube.
There is a command, mapenlarge
, that can grow a map such that the worldroot
takes up a larger volume. The existing map, accordingly, is placed as child 0 of
the new world root, leading to the map expansition occupying space in the +x,
+y, and +z direction from the location of the existing map. This may be slightly
inconvenient for those seeking to expand the scenery bounds of their level
uniformly; a copy and paste of the geometry is needed to re-center the old level
if desired.
While octree subdivision allows for the inclusion of small pieces of geometry, this is not on its own adequate due to the fact that octree nodes are, well, cubes. To allow for maps which have shapes that are not all boxes, Libprimis, like other games in the Cube family, allows for limited, discrete deformation of octree nodes.
Each corner, of which there are eight on a cube, can be deformed along all three directions in steps of 1/8th of the total cube size. This allows for decent approximations of many curves when done carefully, and using different gridpowers prudently can allow for some limited compound curvature.
This deformation, when carried out by all four corners of a cube, can allow for faces which do not align with their boundaries; indeed, all six faces can be deformed in this way to yield a deformed cube that does not touch its neighbors. The "integrity" of the octree node, however, remains intact, and no other geometry can occupy the cube which has been deformed. This set of limitations can be summarized with the following statement:
- Every octree node is defined to have eight vertices and twelve faces.
- Each vertex can only be found at one of 512 discrete points within the node.
- Textures always get an entire face of an octree node.
- Textures are projected from the node normal, not the deformed surface normal.
- Cubes' deformed shapes may not extend past their original undeformed volume.
Therefore, the only way to increase detail in a given area when using cube geometry is to increase the octree node density (by using a smaller gridpower).
For more information on texture projection, see §2.3.3.
The engine automatically attempts to subdivide cubes when a user attempts to place a cube within a node which is of a larger gridpower. This means that many types of cube deformation are no longer possible, causing erratic and generally poor approximations of the prior form. It is recommended to take care when placing new cubes in proximity to existing geometry of a larger gridpower, as this can inadvertently cause unseemly changes to the level's geometry.
The engine, when given a remip
command, attempts to merge nodes with redundant
vertices together such that the renderer has less faces to deal with when later
rendering the scene. This reduces the map size and improves performance.
The amount of remipping intensity is defined by the maxmerge
command, which
determines the maximum gridpower that can be simplified. Having this value too
high causes large surfaces to occlude poorly, as the entire face has to be
textured.
remip
: performs a remip calculation on the level
maxmerge N
: sets the maximum merge gridpower to N
There are several materials in Libprimis which are capable of modifying their volume's properties. Materials in general are combinable (though there are many exceptions) so that multiple effects on the geometry can be combined. Because materials do not have the same deformation ability as geometry, materials are restricted to occupying rectangular shapes and cannot approximate the forms that geometry can.
The most visible apparent materials are the water and glass materials. Water and glass materials (of which there are eight, four of each type) create rectangular volumes of water or glass in their selected regions. Water in particular has features like screenspace reflection/refraction and caustics applied to it in the renderer; glass, on the other hand, allows for screenspace refractive as well as specular effects.
A more versatile but slower type of transparency is alpha material, which flags cube geometry as transparent. The texture's properties determine the effects of marking geometry alpha as opposed to the variables of the material itself; alpha merely tells the engine what areas to treat specially as transparent.
The remaining three types of materials have no visual effect and instead only act on actors and objects in the level. Death material kills all who enter its volume; clipping keeps players out while letting particles through, and noclip keeps geometry from hampering the travel of projectiles and players.
Air, the name for the lack of materials, unsurprisingly is the default
"material" for the level. Air can be "placed" by /editmat air
or /air
whereupon it removes all previously existing materials flagged for that
selection.
The name "air" does not imply that there is any oxygen mechanics in the game and there is no material representing the lack of air.
Water, the material with the largest change with respect to prior engines, has four types that can be modified seperately to apply in different situations on the map. As water is a particularly complex material to physically represent, there are many visual effects provided in the engine which can be tuned for particular situations.
Water is the only place in which screenspace reflection is used in game;
water materials also have options for caustics, reflection, fog, and
environment mapping to complement this. The sides of water material have
different properties (such as lacking screenspace reflection) and are controlled
by a differnt set of variables (the water<N>fall
set).
Commands which apply to the four water types seperately are designated
water<N>
where N is the water material being edited. There are four water
materials, and the first one does not get appended with a "1" like 2/3/4 all
do.
causticscale
causticmillis
causticcontrast
causticoffset
caustics
water<N>color
water<N>deepcolor
water<N>deepfade
water<N>refractcolor
water<N>fog
water<N>deep
water<N>spec
water<N>refract
water<N>fallcolor
water<N>fallrefractcolor
water<N>fallspec
water<N>fallrefract
waterreflect
waterreflectstep
waterlod
watersubdiv
Another one of the four-variant materials, and the second most interesting (after water) with respect to engine features, glass is a cheap and effective way to create transparent geometry. As opposed to alpha material, glass is restricted to rectangular volumes (as with all materials) and therefore is somewhat less flexible; additionally glass material is not capable of backface transparency like glass is. However, because glass material does not require rerendering of parts of the level like alpha does.
Because glass material is expected to be used largely in vertical situations where screenspace reflection is a poor choice, it does not have screenspace reflection support and instead relies entirey on environment mapping and specular mapping for its reflective appearence. This does mean that glass has to be careful about how it is placed such that it falls within the radius of an environment map entity. Like water and alpha materials, however, glass is capable of screenspace refraction, useful for nonflat materials.
In itself, glass material does not block movement, but in practice it always does because clipping material is mandatory for all types of glass. The engine will automatically place clip wherever glass is placed.
glass<N>color
Tint color of the glass materialglass<N>spec
Specularity (glossiness) scale of the glass materialglass<N>refract
Refraction (light distortion behind) of the glass material
An entirely transparent material, clip, unlike the materials prior, does not affect the rendering of the scene in any way. Clip, instead, impedes the ability for actors (players, bots) to enter their volumes; it allows for the map to be securely blocked off from leaving even without geometry placed. Clip material is also recommended for cases where the existing geometry has troublesome collision, such as trellis or crosshatched geometry. Clip is also always placed wherever glass is, to make it impossible to phase through.
Clip, however, notably does not impede the progress of particles or projectiles, which allows it to be used for map boundaries without fear of random projectiles bouncing or dying on collision with its bounds. If it is required that projectiles be deleted upon contact, using death material in tandem with clip is a viable solution.
The opposite material to clip (unsurprisingly), noclip instead permits the passage of actors through otherwise impenetrable geometry. Noclip additionally allows projectiles through surfaces, making the enclosed volume get treated essentially as air instead of having whatever collision the geometry within has.
As a result, noclip is of no effect for volumes not containing geometry; there must be geometry within the noclip's volume for it to take effect.
Using noclip effectively and not having immersion-breaking visibility problems generally limits its use to relatively small applications, like flattening out a floor or smoothing a wall out so that it is not obstructing travel. When placing noclip, it is advisable to check it by spawning into the level and making sure that there is no unseemly visibility issues (where you can see through the wall) that would break immersion.
Death material forces the suicide of those players who enter its bounds, instantly killing them.
Death material also destroys particles and projectiles which enter its midst, which is potentially useful for culling unnecessary particles that enters its region.
The bottom of the map (z < 0) also acts as death material and players who leave the bottom of the map automatically are killed. No such effect takes place on the sides or top of the map volume.
No GI material flags its volume as not being lit by global illumination. This does not have a material impact (or improvement) in performance but may be useful to combat artifacts in the low-resolution global illumination algorithm.
Note that those regions beyond the radiance hints far plane (rhfarplane
) will
be lit regardless of their No GI status. Keep this in mind whenever placing
long-distance radiance hints.
Alpha material is the more versatile but slower companion to glass for the creation of transparency. Alpha material draws cube geometry within its bounds as transparent, allowing for transparent shapes in forms other than rectangular boxes.
Alpha material has its material properties dependent upon the flagged settings
of the geometry contained therein; using valpha
allows for changing a
texture's transparent-ness. The material on its own has no flags or other
related commands to modify its behavior.
Alpha material does not support continuous variable opacity, and the opacity is always constant for a given texture. While this is certainly a limitation, typical transparent objects like windows generally have constant opacity across their full area.
The faces of cubes within the game can be given textures on a cube face by cube face basis, allowing for immersive, complete scenes to be generated via cube geometry.
Textures are applied to the six faces of the cube with a simple planar
projection; as a result, there is distortion when cubes themselves are
distorted. This can be allieviated with the more expensive triplanar
shader,
but that is beyond the scope of this section.
Each cube has a texture defined for each of its six faces; this means that
"buried" geometry will after revision cause the storage of meaningless texture
information for invisible geometry. For this reason, there is a command
fixinsidefaces
which can set all invisible faces to the default texture.
fixinsidefaces [vslot]
Sets all invisible faces to the vslot given.
Textures are registered to a file that accompanies the map, generally named
the same as the map file and with the .cfg
extension. Texture slots are for
unique shader combinations (for a description of the shaders see the section on
texture rendering) and as a result at least one physical slot is required for
each distinct texture in the game. A texture slot declares the following:
- Shaders used in the texture
- Modifications to shader paramaters
- Relevant texture maps
An example of a typical texture declaration is shown below.
setshader bumpenvspecmapglowworld
setshaderparam envscale 0.7 0.7 0.7
texture 0 "nieb/complex/light01_c.png"
texture n "nieb/complex/light01_n.png"
texture s "nieb/complex/light01_s.png"
texture g "nieb/complex/light01_g.png"
This shader is set to bumpenvspecmapglowworld
, meaning it uses bump (normal),
environment mapping, specular mapping, and glow mapping. The world
on the end
is to declare it as a cube geometry shader as opposed to a decal. Afterwards,
the four relevant maps are provided for the different shaders that are to be
applied with the texture
command;
0/c
declares a diffuse mapn
declares a normal maps
declares a specular mapz
declares a height (parallax) mapg
declares a glow map
These definitions of textures are largely set beforehand and then called with
texload
upon running the map's config (automatically run at map load); most
textures have only a couple of possible appropriate shader combinations anyways.
Virtual slots encode simple manipulation of textures, such as coloration, scale,
rotation, and orientation. These do not require declaration upon map generation
and are generally created dynamically ingame after the execution of a v
command. Virtual slots (vslots) then save only the parent texture index and the
modifications done to it; they save significant memory space by not requiring
a modified copy to be stored in video memory.
The modifications that a vslot can store are described below in the V-command section.
The standard scaling of textures is such that there are 512 linear pixels per
gridpower 5 cube, leading to a density of 512/32 = 16 pixels per power 0 cube
or 512/4 = 128 pixels per meter. This is somewhat low but largely sufficient
for generic areas which the player does not find themselves particularly close
to, but may be insufficient for areas the player is near to; for this, the
V-command vscale
is very useful.
Textures which are not square are projected faithfully and there is no stretching of the shorter axis; this means that trim textures can be made skinny and narrow if desired to save space.
Textures are projected onto the parent node normals and not the deformed surface normals, which causes distortion of the texture when it is heavily distorted. This effect additionally causes attempted blending of faces (like when trying to make rounded organic geometry) to have an unseemly seam along these boundaries, as these boundaries delimit different texture projection differences.
This problem can be solved with the triplanar
shader, which forces textures to
be projected in three different directions such that the true normal caused by
the distorted cube can be found accurately.
The V-commands are a set of texture modification commands that allow for textures to be flipped, rotated, scaled, tinted, and offset as necessary. These commands, when run while in the editor, create new vslots with the modified behavior.
The tex-commands are the corresponding commands for standard texture slots and have the same effect; these are declared for physical texture slots as opposed to being dynamically assigned to virtual slots (vslots).
Other than their means of assigning texture behavior to slots, however, the two commands are otherwise identical in their behavior. V-commands are the ones used ingame; tex-commands are generally placed in texture definitions.
alpha
sets the amount of transparency to render the texture if it is within
the volume of placed alpha material. The property has no effect otherwise, and
so modifying this should only be done while the texture is inside alpha material
such that the effects of changing alpha
are apparent.
Textures are set to 0.5 0
alpha by default, meaning they are halfway opaque.
Notably, the second parameter is zeroed out, such that the backface alpha
feature of the engine is not enabled. Backface alpha is the closest thing the
deferred renderer of Tesseract (deferred renderers in general have trouble
dealing with alpha in expedient ways) to multiple layers of alpha, though it
is a somewhat limited approach which does not lend itself to wide applicability.
Backface alpha allows the far side of the geometry to also be rendered along with the standard front face that is rendered by default. Backface alpha requires another geometry pass in the renderer and is measurably slower than leaving it off, but also is the only way to simulate glass-behind-glass in levels.
angle
rotates the texture by a given angle; capable of rotating textures
by arbitrary amounts through 360 degrees. If needed, this can be combined with
rotate
which works by a different mechanism (and is the only way to get
flipped/transposed textures).
color
changes the color of the texture evenly through the values of the three
parameters passed to it. color 1 1 1
is the identity operator (has no effect)
and values above and below this will change the colors accordingly. As the
combination of red, green, and blue is the standard basis for additive colors
(like on a monitor), any color can be created by using the three channels
appropriately.
offset
offsets a texture by a given number of pixels; this means that higher
resolution textures need larger offsets, and that for standard textures,
fractional offsets are in powers of 2 (a 1024x texture needs to be offset by
512 to be shifted by half a texture).
refract
modifies the refractive behaviors of materials that are within alpha
material. Refraction is the distortion of rays or light traveling through a
material due to the change in the speed in light at material boundaries, and is
handled in Libprimis via screenspace effects. The intensity of the refraction
is handled by the scale
parameter and the color of the refraction is
controlled by the R G B
parameters; 1 1 1
is the default white color.
rotate
transforms a texture by the possible "simple" 2x2 matrix transforms,
of which there are seven.
- 1 rotate 90
- 2 rotate 180
- 3 rotate 270
- 4 flip x
- 5 flip y
- 6 transpose
- 7 flip and transpose
Note that flipping and transposing are the same regardless of whether the flip is over the x or y axis.
These transforms are basic means of getting new orientations for situations
which do not require the more granular vrotate
command and is the only way
to flip/transpose textures.
scale
changes the size of the texture linearly along its axes. As a result,
a texture at vscale 4
takes up four times the area as vscale 2
while having
linear dimensions twice as great.
The identity setting for scale
is scale 1
; the default scale factor,
perhaps not surprisingly, is unity. The engine does low for scales smaller than
unity, which corresponds to downscaling the texture to increase its density.
The limits for scaling correspond to powers of two; the minimum is 2^-3 (1/8)
and the maximum is 2^3 (8). At these extremes, textures are either way
overdetailed (1024 pixels per meter) or way underdetailed (16 pixels per meter).
For most applications, it is recommended that the scaling be kept to powers of 2 such that the texture tiles in sync with the cube grid. Exceptions where other scales may be appropriate include instances where 3/2 scaling is desired for a 3 cube wide area or organic textures which are not intended to noticibly tile.
scroll
causes the texture to take on a linear time-varying offset such that
the texture appears to move with respect to the surface it is applied to. The
scale for this scrolling effect is such that scroll 1
is 1 texture per second;
this is usually too large for common scrolling objects (like banners or
conveyor belts) and as such fractional values here are most commonly employed.
The world in Libprimis has many global variables that affect the entire level evenly. These include ambient lighting, fog, and skybox settings, as well as more technical aspects such as mipping intensity. This section does not include the global settings for individual materials, as is covered in §2.2.
Sunlight, the cheapest form of bulk lighting in the game, is a dynamic light which casts shadows like any other light, but from a projection at infinity. Sunlight therefore projects perfect quadrilaterals from rectangular objects (as opposed to the trapezoids of point lights on the level).
Sunlight is also the only type of light that takes advantage of Libprimis' global illumination capability, as enabling it for standard lights is too expensive. Global illumination by sunlight is capable of providing ambient lighting to partially lit rooms and is faster than using large numbers of on-level point lights.
Sunlight has just four variables controlling its behavior, which set its size, color, and location.
sunlight <color>
Sets the color of the sunlight, passed as a hex color.sunlightpitch <angle>
Sets the sun's inclination angle above the horizon.sunlightyaw <angle>
Sets the yaw angle (about z axis) of the sunlight.sunlightscale <scale>
Sets the intensity scale of the sunlight.
Fog is an effect that fades objects to a particular color as the distance to that object grows larger. Fog is useful for creating a closed, damp ambiance, but be aware that it can be easily disabled on client machines, making its use as a balance technique inadvisable in light of its lack of security with respect to its implementation.
Fog also culls the rendering of entities once they pass into the realm of
complete obscurance, which is set by the fogcullintensity
variable.
fog <dist>
The characteristic distance for the onset of fog effects.fogcolor <color>
The color of the fog, as a hex color.fogcullintensity <scale>
The intensity by which fog culls entity rendering.fogintensity <scale>
The fog effect intensity (lower values -> more fog).fogoverlay
The cheapest type of global lighting, changing the ambient light level for the map makes everything at least as bring as the set ambient level. This reduces contrast however and makes the shadows less deep, so generally using at least global illumination combined with sunlight is preferable whenever possible to high ambient settings. A little ambient, however, is advisible to make the scene not pitch dark anywhere.
Ambient lighting does not require shading resources like sunlight or point lights and so is not a performance issue like the other types of lighting can be.
ambient
Hex color for ambient color, typically dimmer than0x333333
ambientscale
Multiplier for ambient color (usually left at 1)
The skybox is a static rendering of a scene surrounding the map which provides a backdrop to the level. The skybox is a cubemap, a type of environment projection wherein the viewable area around a point (a full sphere) is seperated and projected onto the six faces on a cube. The cubemap projection is particularly convenient for its simplicity of projection for the engine and its relatively low distortion (as opposed to a single-face projection like Mercator) while remaining fairly simple to comprehend.
Skyboxes in Libprimis are passed as a set of six images:
- bk: the backside texture (normal facing south)
- dn: the bottom texture (normal facing upwards)
- ft: the front texture (normal facing north)
- lf: the left texture (normal facing west)
- rt: the right texture (normal facing right)
- up: the top texture (normal facing downwards)
Note that these are defined for cases in which the yaw of the skybox is 0; that is to say that it it has not been rotationally translated at all. Rotating the sky about the z-axis is possible and changes the orientation of the side faces of the skybox accordingly.
These six textures are loaded whenever the skybox is set: setting skybox foo/bar
will automatically load foo/bar_bk
, foo/bar_dn
, etc. as the skybox.
Implicit in the path is the location of skyboxes in /media/sky
.
skybox <path>
Sets the path of the skybox, excluding the _XX and format.skyboxcolor <color>
Tints the skybox to the given hex color.skyboxoverbright <scale>
Controls how bright highlights in the skybox are.skyboxoverbrightmin <scale>
Sets the overbrightness overall of the skybox.skyboxoverbrightthreshhold <scale>
Sets min brightness for highlighting.skyboxyaw <angle>
Sets the overall orientation of the skybox in the world.skyboxspin <angular vel>
Sets the rotation speed of the sky in deg/s.
Additionally, the engine supports a single planar layer of clouds. The "height" of this layer is adjustable, but there is no parallax regardless of height: moving around on the map will not change the relative position of the cloud layer. The cloud layer instead gains its apparent closeness from the rate at which low-inclination clouds become apparently smaller: lower cloud layer heights mean that the center is relatively larger compared to higher cloud layer heights. Additionally, lower cloud layers appear to extend closer to the horizon than higher ones.
Cloud layers are, unlike the cubemap projections that cloudboxes and skyboxes use, able to scroll (have a translational movement with respect to time) in addition to being able to spin about the z-axis. This allows for somewhat realistic cloud movement when done in moderation.
cloudalpha <value>
Sets the opacity of the cloudlayer (0..1, 1 for opaque).cloudclip <value
Sets level of clipping passed to env box draw.cloudcolor <color>
Tints the cloudbox to the given hex color.cloudfade <value>
Sets the fade rate of the cloudbox.cloudheight <value>
Sets the apparent height of the clouds in the sky.cloudlayer <path>
Sets the path to the cloud layer, excluding extension.cloudoffsetx <value>
Sets the x offset amount, in pixels.cloudoffsety <value>
Sets the y offset amount, in pixels.cloudscale <value>
Sets the scale factor of the clouds (1 by default).cloudscrollx <value>
Sets the x scroll amount, in pixels/s.cloudscrolly <value>
Sets the y scroll amount, in pixels/s.cloudsubdiv <value>
Sets the number of edges the cloud perimeter has.cloudspin <value>
Sets the spin rate of the clouds in the CW direction.cloudyaw <value>
Sets the yaw offset angle of the cloud layer.
Atmo is the way that the game can create a procedural skybox such that the sky follows the sun's position and lights itself according to the sun's position. The sun (which as noted in §2.4.1 is set by sunlightpitch/sunlightyaw) determines the location of the atmo sun in the sky, making the atmo skybox always line up with the sun's position and the shadows it throws.
The atmo layer can be blended with the skybox layer with atmoalpha
and
as such can be blended with complex scenes while still being capable of dynamic
positioning whenever needed. Standard skyboxes with fixed light sources are
restricted to whatever pitch the light source is placed on the skybox (though
the skybox can be rotated about the z direction) and this limits the ability
of sunlight to properly light a scene.
Atmo takes many physical parameters which affect the simulated atmosphere that is created. These include the opacity of the air, the size of the planet, the apparent intensity of the light source, and the characteristic color of the sky.
atmoalpha <value>
Sets the opacity of the atmo layer (0..1, 1 for opaque)atmobright <value>
Sets the overall brightness of the atmo sky.atmodensity <value>
Sets the diffusion amount of the atmo air.atmohaze <value>
Sets the scale of haze at the horizon.atmohazefade <value>
Sets the color that the horizon haze fades to.atmohazefadescale <value>
Sets the haze transition scale.atmoheight <value>
Sets the height of the horizon atmospheric effect.atmoplanetsize <value>
Sets the hardness of the atmo transition.atmosundiskbright <value>
Sets the brightness of the atmo sun.atmosundisksize <value>
Sets the diameter of the atmo sun.atmosunlight <value>
Sets the color of the atmo sun & overall sky color.atmosunlightscale <value>
Sets the ratio of the sunlight brightness vs atmo.
Because of the Libprimis engine's recursive octree geometry format, it is not practical to save levels in a standardized polygon soup format like GLTF. Instead, Libprimis saves levels in its own format, the general details of which are explained in this section.
The file is a gz-compressed stream of data, with the following main components:
- A map header with the sizes of the non-geometry part of the data
- A list of map-specific variables, such as enviornment settings
- A list of entities with their associated information
- A list of virtual texture slots "vslots" which map surfaces to texture indices
- An octal tree of cube nodes which make up the level's geometry.
The header tells the engine how many of particular quantities show up in the body of the map file. The header is of fixed length and encodes the following information:
- an identifier of four chars
- the version of the map
- the size of the header itself
- the size of the level in powers of two
- the number of entities saved on the level
- the number of PVS saved on the level (not used by the engine)
- the number of blendmaps saved on the level (not used by the engine)
- the number of variables saved on the level
- the number of virtual texture slots (vslots) saved on the level
The number of octree nodes, the actual geometry count on the level, is not saved to the map header. This is practical because the octree nodes are the last part of the level file (and so nothing needs to have its location indexed beyond it), and because the octree nodes themselves are recursively loaded due to the octal tree structure of how they are related to each other.
The map saves a list of the variables which are to be modified away from the engine defaults. As the engine defaults may change over time, only variables explicitly defined as different in the level get saved here and as a result undefined variables may lead to inconsistent behavior with different versions of the engine.
These variables all have a single float, string, or integer value associated with them and can encode one feature of the level, such as cloud information, skybox settings, or ambient settings.
There is a technical limit of 65636 changed variables for a map due to the size of the data type (ushort) used to index variables, but this is far in excess of the number of variables that the engine has in total.
The static map entities (such as lights, spawns, etc.) have their location saved to the level as well as the values of the five attributes which can modify them. Only static entities, like those placed by a mapper, are saved here.
There is a limit of ten thousand entities maximum that are allowed to be saved
to the level via the MAXENTS
macro. This limit of 10,000 is somewhat arbitrary
and not especially technical, but in no realistic circumstance should this be an
actual limit for reasonably usable maps in the engine (other engines are likely
better for these types of applications).
The architectural limit for the number of map entities is 65636, the same as for map variables, but hitting this limit would require redesign of other parts of the engine to more efficiently process gigantic numbers of entities.
Very large numbers of entities run into performance problems before these
technical barriers are hit, as the engine is not designed for especially large
entitiy counts. Typical numbers of map entities on maps range from several dozen
to several hundred, and the maximum limit imposed, either by the MAXENTS
macro
or the architectural limit, is far in excess of reasonable level making in this
engine.
Following the two (relatively straightforward) lists of variables is the virtual texture slot (vslot) list. This list defines a set of indices which are the values given to the individual cube faces. These indices are much smaller than the actual texture and shader information that they represent, compacting the amount of data required for each cube object to hold. There is on the order of ten thousand to a million cubes on a typical level, making it much more terse to map them by a list of slot indices.
The indices defined in the level do not map to texture information in the map's zipped file: it is instead defined in the associated configuration file which is usually provided along with the map file. This configuration file runs a script which tells the engines which texture & shader information to allocate to a particular slot; this is not filed by the map binary itself.
The static entities are world elements which get saved to the level and are loaded on game start. These entities have the following types:
- light: a point light source
- mapmodel: a contained piece of geometry
- playerstart: a location where players spawn from
- particles: a location where the engine generates dynamic particles
- sound: a point sound source
- spotlight: a linkable modifier for light entities
- decal: texture application onto a geometry face
- teleport: a location that moves the player to a teledest
- teledest: the output location of a teleporter
- jumppad: object that pushes actors around
- flag: flag for capture-the-flag gameplay
Entities themselves are point objects which can be placed at any arbitrary location within the level. Their effects generally extend to a radius beyond just the point that the occupy, including defined radii for entities like lights or sounds or simply having a defined structure as mapmodels do. Entities are always themselves static on the level and do not move unless manipulated by an editor; however they can have dynamic effects which may make them appear to move.
Entities in Libprimis all have five attributes each, though not all attributes are necessarily defined for a given model. The consistency of ent attributes is designed to make user interface design as straightforward as possible such that complex dynamic methods for menus are not necessary to edit entities.
There is no practical limit to the quantity of entities a level can have; for many common types of entities, performance concerns make any physical limit moot. There exists issues with very excessive numbers of lights overfilling the light buffer or very excessive numbers of sounds causing sound issues, but these are not a concern until the level is already unplayable.
Lights are entities where light appears to eminate from. Lights are point entities and the light they cast is as from a perfect point source. Because the engine is deferred and dynamically lit, light counts are one of the largest influencers of performance, and the engine automatically occludes lights by a tile-based algorithm to not render those lights which do not contribute to the scene. Light entities' performance is highly related to its radius, and therefore use of large light entities for bulk lighting is not recommended and use of sunlight and global illumination is recommended in its place.
Light entities do not benefit from the enhancements that screenspace ambient occlusion and global illumination have on the sunlight, as these features are too expensive to enable on a light-by-light basis. For similar reasons, only point lights are supported, as it is in fact very difficult to create lights with configurations more complex than ideal point radiators.
As lights are one of the key cogs of the deferred renderer used in Libprimis, a more technical discussion of their behavior with respect to the rendering pipeline can be found in that section.
Lights have five attributes, the last of which itself has a set of flags which control the light's technical behavior.
The maximum distance the light entity can cast light; strongly related to performance impact of the light and shadow map usage
The radius of the light is, as with other distances, defined in cubits.
The intensity of the red channel of the light's output. Nominally, 255 is "full" red, but this can be exceeded for an overbright light.
The intensity of the green channel of the light's output. Nominally, 255 is "full" green, but this can be exceeded for an overbright light.
The intensity of the blue channel of the light's output. Nominally, 255 is "full" blue, but this can be exceeded for an overbright light.
Lights support four flags which can be combined to achieve particular effects.
- 1
noshadow
: treats geometry/models as transparent, allowing light through - 2
static
: disables shadow map updates, causing static shadows - 4
volumetric
: simulates light reflection off of dust in the air - 8
nospec
: disables specular highlights
Mapmodels are entities which represent a 3D model. While this object can be animated (e.g. a fan or reciprocating device) it cannot undergo reactive or scripted behavior and the entity itself remains at one point at all times.
Mapmodels are usually supplied in the md3 (Quake III), obj (Wavefront), or iqm (Inter-Quake Model) formats; they additionally need one or more u,v maps (UV maps) to define their surface normals, speculars, and specularity. Unlike level geometry, mapmodels do not support parallax mapping.
Mapmodels in Libprimis have support for hitboxes which closely mirror that of the physical model; however, mapmodels do not support decals and as a result weapons hitting mapmodels do not leave bullet marks like ordinary geometry does.
Mapmodels have controllable attributes for their size and orientation; the particular mapmodel to be used is given by an index. Note that the possession of only three degrees of freedom means that the model can become gimbal locked if orientation values are chosen poorly.
Selects the index of the passed models which are loaded into the map to display. As usual, this index begins at 0 and counts upwards; the engine will simply display nothing if there is no valid model at the index.
Mapmodels are generally defined in map configuration files and therefore the specific model assigned to each index is not enforced game-wide.
The yaw (azimuthal) angle of the model, in left-handed (clockwise) degrees. Values larger than 360 can be passed but are identical to passing in their modulus 360.
The pitch (altitude) angle of the model, expressed as an inclination from the horizon. Negative values can be used to pitch the model towards the -z axis.
The roll angle of the model, expressed as a right-handed rotation about the
axis set by the pitch
/roll
attributes.
The scale factor of the model. Scaling is always isotropic (no distortion) and the identity point is at 100 (100 is "normal" scale) as opposed to 1 for many other engine features; this is because the arguments passed to entities are always integers (and obviously setting 1 as unity w/ only integral steps would be not quite optimal).
The playerstarts define where players respawn after they are killed. Unsurprisingly, playerstarts have a team associated with them which determines the group of players who are allowed to spawn there; team spawns only can spawn the players belonging to that team.
Playerstarts have attributes which define the orientation of players who spawn at them. The playerstart entity has only two relevant attributes; the last three attributes have no effect on the behavior of the entity.
0 for FFA, 1/2 for teams blue and red respectively. Available spawns for any arbitrary player are limited to playerstarts who share the same team index.
The yaw (azimuthal) angle of the player when they spawn, in left-handed (clockwise) degrees. Setting the yaw of the playerstart is important to prevent players from spawning facing the wrong way, such as towards a wall.
The six types of implemented particles use their five attributes differently.
As a result, this section is has its last four attributes' descriptions
seperated into sections by the type of particle in use (which is set by
parameter 0 type
).
Particles do not collide with players or are dynamically manipulated by physics; they are, however, culled upon collision with cube geometry to minimize wasteful rendering of particles which cannot be seen.
Particles are billboards, meaning that they are 2d objects which always have their normal vector pointed at the camera. For this reason, particles' orientation is determined by the camera, not the scene, and for objects that are usually anisotropic (like fire, which only makes sense in one direction), this approximation can yield some poor results. For this reason, maximum particle size is generally kept fairly small to prevent the obviousness of billboarding.
Additionally, particles are client side effects, meaning that one person's view of a particle is not representative of the effect rendered on other people's machines.
Easily the most complex entity with respect to its attributes, particles have
unique specifications for each value passed to its first attribute type
. This
means that particles cannot have their type
changed and have attributes
consistently transfer.
The type of particle for the game to render. There are six types implemented:
- 0 fire
- 1 smoke
- 2 water
- 3 plasma
- 4 tape
- 5 status
Fire particles create a vertical plume of fire, with customizable footprint, particle size/total height, and color. Fire particles also have nonoptional smoke which appears at the top of the plume.
Smoke particles create a slightly directional gray cloud of smoke, with a direction selectable along six directions (the three coordinate axis directions and their negatives). Smoke cannot have its color changed, and is always a moderate shade of gray.
Water particles create a small fountain effect which can, like smoke, have its direction selectable from the aformentioned six directions. Water can have its color changed to suit the liquid being represented, but the physics of the particle movement are fixed except for their orientation.
Plasma particles are a large, brightly colored ball of gas which hovers about the entity point. Unlike other particles, plasma does not spawn in multitudes and then fade; it remains a hovering entity at all times, with its only variation being a pulsing effect.
Tape particles are a raylike particle type that creates light beams along a specified direction. While they are restricted to the same six directions as water and smoke, there is a large variety of modes which they can occupy for each direction, including plane, sphere, cone, circle, and ray configurations.
Status particles display a billboard displaying a bar which can be configured to be filled between 1 and 100. Through scripting, it is possible to dynamically change this value (by manually editing the entity attributes e.g.) to make it dynamically display some desired gameplay aspect. Status particles can have their bar color set, but the background and outline colors are fixed.
Each of these has different attributes 1-4 and obviously shows the particle type aformentioned.
For fire and plasma, the radius paramater controls how large the particle can be. For fire, this is the areal size; the size that the "base" of the flame occupies. This area is always a perfect square (meaning many entities are required for a potential rectangular shape).
Likewise, for plasma, this parameter controls the radius of the sphere bounded by the plasma effect.
For smoke, water, and tape, this parameter defines the orientation of the particle. Only tape takes into account values above 5 or the directionality of the particle; water and smoke are restricted to "ray" type configurations only.
The lowest valid index for each particle configuration is listed in the table below:
- The value is the number put in the
type
attribute of the entity. - The type is the geometrical shape of the configuration.
- The orientation describes the way the whole configuration points.
- The direction is the orientation of individual rays relative to the origin.
Value | Type | Orientation | Direction |
---|---|---|---|
0 | Ray | +z | Away |
1 | Ray | +x | Away |
2 | Ray | +y | Away |
3 | Ray | -z | Away |
4 | Ray | -x | Away |
5 | Ray | -y | Away |
256 | Circle | xy plane | Away |
257 | Circle | yz plane | Away |
258 | Circle | xz plane | Away |
259 | Ring | +z | Normal |
260 | Ring | +x | Normal |
261 | Ring | +y | Normal |
262 | Cone | +z | Away |
263 | Cone | +x | Away |
264 | Cone | +y | Away |
265 | Cone | -z | Away |
266 | Cone | -x | Away |
267 | Cone | -y | Away |
268 | Plane | +z | Normal |
269 | Plane | +x | Normal |
270 | Plane | +y | Normal |
271 | Line | xz (+z) | Away |
272 | Line | xy (+x) | Away |
273 | Line | yz (+y) | Away |
274 | Line | yz (+z) | Away |
275 | Line | xz (+x) | Away |
276 | Line | xy (+y) | Away |
277 | Sphere | +z | Away |
278 | Sphere | +x | Away |
279 | Sphere | +y | Away |
288 | Circle | xy plane | Towards |
289 | Circle | yz plane | Towards |
290 | Circle | xz plane | Towards |
291 | Ring | -z | Normal |
292 | Ring | -x | Normal |
293 | Ring | -y | Normal |
294 | Cone | +z | Towards |
295 | Cone | +x | Towards |
296 | Cone | +y | Towards |
297 | Cone | -z | Towards |
298 | Cone | -x | Towards |
299 | Cone | -y | Towards |
300 | Plane | -z | Normal |
301 | Plane | -x | Normal |
302 | Plane | -y | Normal |
303 | Line | xz (+z) | Towards |
304 | Line | xy (+x) | Towards |
305 | Line | yz (+y) | Towards |
306 | Line | yz (+z) | Towards |
307 | Line | xz (+x) | Towards |
308 | Line | xy (+y) | Towards |
309 | Sphere | +z | Towards |
310 | Sphere | +x | Towards |
311 | Sphere | +y | Towards |
For a status particle, this parameter defines the particle's fullness, as a range between 0 and 100. At 100, the bar is full; values above this have no additional effect.
For water, plasma, and status entities, parameter 2 specifies the particle's
color. This is passed as a hexadecimal triple (0x000
...0xFFF
) which
specifies the color of the particle to four-bit precision. While four bits per
channel is indeed very low compared to common pictographic formats (typically
256 colors per channel), in practice particles (which are essentially
monochromatic) do not need a gigantic amount of distinct colors (there is
essentially no difference between 0xF00 and 0xE00 that matters when picking a
slab of color).
For tape particles, this parameter specifies the length of each tape particle.
This does not meaningfully affect the width of the tape particles, only its
length, and so the only way to reduce the apparent thickness is instead by its
color
parameter.
For fire particles, this parameter defines the height and size of the fire particles. Larger fire particles last proportionally longer (and this ratio is fixed) and therefore rise to a greater height before fading.
This parameter has no effect on smoke particles and any value specified will be ignored.
Tape has its color parameter on the third attribute, and it works in the same hexadecimal triple form as the above explaination of color for the other particle types. In the same way, the color of the fire particles' flames can be set. The smoke that fire particles release cannot be modified.
None of the other entities take this attribute into account and setting a value for any of them will be ignored.
Only tape particles take this parameter into account; fade sets the time in milliseconds for the particle to delete itself once it has been spawned. Tape particles fade out gradually and do not get jarringly deleted at the specified time. Note that the fade out rate is a fixed ratio of the total particle lifetime and therefore quicker when the tape particle is set to die out relatively quickly.
For all other particle types, this parameter can be set to any value but will be ignored.
Sound entities place a static, looping sound effect at the point where the entity is placed. Sound entities can only have their volume modulated globally and by radius. Sound entities take a sound index which defines the entry in the engine's mapsound list which is played; this is generally set in the map's configuration file which is executed at map start. This sound entity is then triggered upon a player entering its characteristic radius, playing the set sound (locally) at them. Sounds are not ensured to sync perfectly across different clients.
Sound playback from entities does take into account location of the entity and therefore plays back the sound in stereo, with the sound intensity per channel defined by the location of the entity.
There are two attributes for the sound entity, the index of the sound and radius at which it starts playing.
The index of the sound entity indicates to the engine which of the set sounds declared in the map configuration file is to be played by the entity. This index counts up from zero (negative values never index a valid sound) and values beyond the number of indexed sounds simply fail silently (heh) and no sound effect is played.
The radius within which the game will play the index-defined sound at the player. This radius, as always, is in cube units of 1/8m, and the bounding surface for sound entities is always a simple sphere, represented in edit mode by a wireframe bounding indicator. At distances beyond this radius, sound playback from the entity is automatically rejected and the sound entity does not playback in any form.
Spotlight entities require linking to standard entities, such as by entlink
.
Once attached to a light, the spotlight acts as a modifier to the light entity,
creating a directed cone of light. The spotlight's lone attribute controls the
spread of the cone, and the vector from the light entity to the spotlight sets
the axis which the spotlight is oriented along. The location of the spotlight
otherwise has no effect upon the behavior of the spotlight's cast beam.
Notably, spotlights cause the light entity in question to switch from cubemapping to planar mapping, resulting in issues when surfaces are normal to the plane of the mapping (which is tangent to the point of the cone and normal to the axial vector); since the mapping has no detail along the normal direction, there exists aliasing issues along that face, and as a result large cone sizes which project onto faces parallel with the spotlight vector is not recommended.
The lone attribute for the spotlight entity determines the spread of the cone of light. An implicit attribute, the position of the entity, sets the direction of the beam (measured relative to the location of the linked light.
This attribute sets the spread of the cone of light set by the spotlight entity. The overall inside angle of the cone is equal to twice the value of this attribute; the attribute measures the angle between the edge and center of the beam. This attribute is capped at 90 degrees: as a result of single-plane mapping, "spotlights" with a cone of light beyond 180 degrees is not representable with the projection.
Decals are static entities which act to project an image (specified by an index) onto a surface of cube geometry. The limitation to cube geometry is an important one, one that precludes the usage of mapmodels that can be manipulated easily within the engine. Stains, which are used by weapons, can place their images upon any type of geometry, including mapmodels.
Decals are loaded into the map's configuration file in the same manner that textures are. The order in which the decals are defined sets their map-specific index, which is then referenced when specifying the image used by the decal entity.
Decal entities have all five attributes contribute to the entity's behavior: the
first is the decal index, declared in the map configuration, while the last four
determine the orientation (1,2,3) and scale (4) of the decal. This is the same
set and order of attributes used for the mapmodel
entity.
Selects the index determining which of the decals that are loaded into the map are to be displayed. As usual, this index begins at 0 and counts upwards; the engine will simply display nothing if there is no valid decal at the index.
Decals are generally defined in map configuration files and therefore the specific decal assigned to each index is not enforced game-wide. The decal's index is simply allocated by the position of the decal's reference relative to other decal references (the first mapmodel declared in the configuration file is indexed 0, the second one 1, etc.).
The yaw (azimuthal) angle of the decal, in left-handed (clockwise) degrees. Values larger than 360 can be passed but are identical to passing in their modulus 360.
The pitch (altitude) angle of the decal, expressed as an inclination from the horizon. Negative values can be used to pitch the decal towards the -z axis.
The roll angle of the decal, expressed as a right-handed rotation about the
axis set by the pitch
/roll
attributes.
The scale factor of the decal. Scaling is always isotropic (no distortion) and the identity point is at 100 (100 is "normal" scale) as opposed to 1 for many other engine features; this is because the arguments passed to entities are always integers (and obviously setting 1 as unity w/ only integral steps would be not quite optimal).
Unlike static entities, projectiles are not created directly by mappers and are instead created primarily by weapons as their fired projectiles. Projectiles are synced across the server (as befitting their usually deadly nature) and carry a number of properties befitting this which are distinct from static entities.
Projectiles have eleven attributes encoded within them which define their entire behavior from birth to death. Projectiles are created when a weapon fires, and as a result they have owners and attack data.
- vec
dir
direction that the projectile is pointed - vec
o
location of the projectile currently - vec
from
world coordinates where the projectile starts - vec
to
world coordiantes where the projectile ends - vec
offset
displacement from from/to vec path defined - float
speed
speed of projectile in cubits/s - gameent
*owner
player who created the projectile - int
atk
attack type of the projectile - bool
local
multiplayer sync flag for the projectile - int
offsetmillis
time delay for the projectile - int
id
unique identifier for the projectile
There are five vector attributes which describe the orientation and velocity of
the projectile. These are the dir
ection, lo
cation, from
originating
position, to
destination position, and offset
displacement from path.
The first attribute of these is the dir
ection, which determines the
orientation of the projectile. This vector determines the orientation of the
projectile in space, and is set to point along the direction of the target
(parallel to the velocity). As this vector does not imply a magnitude, it should
be a normalized (magnitude of 1) vector. This vector is in world coordinates, as
there is no independent coordinate system for projectiles defined by their
movement. As a result, corrections to a projectile entity must come from manual
changes to the direction of this parameter.
As this attribute is a 3 dimensional vector, there is no way of controlling the roll of the projectile. As a result, spiraling projectiles are not possible.
The second attribute is the lo
cation of the projectile currently. At t=0 this
parameter is set to be the same as the from
parameter, and the lo
cation at
later times evolves towards the to
location with respect to time. This vector
is expressed in the world coordinate system, which is the same for every
projectile.
The third attribute is the place where the vector is from
. This is, for a
weapon-generated projectile, the location that the gun is when the projectile is
created. This vector is also identical to the lo
cation vector at the time when
the projectile is created.
As with the lo
cation vector, this vector is expressed in terms of world
coordinates, and all projectiles share the same coordinate system.
The point on the map where the projectile is pointed towards. This is, for a
weapon-generated projectile, the location at a distance given in the range
parameter; this point may be inside geometry, in which case the collision
checker will destroy the projectile at that point.
The offset
vector determines the location the projectile appears to originate,
as a distance away from the actual start position. This is used to make the
projectile appear from the gun rather than from the player's coordinate, and as
a result this attribute, unsurprisingly, is a displacement rather than a
position vector.
The non-vector attributes that projectiles posess include the projectile's speed (which does contribute to the not-explicitly-defined velocity vector), the owner of the projectile, the attack type of the projectile, the locality flag, the delay time for
The speed
of the projectile is the rate, in cubits, that the projectile moves
through the world. This, along with the direction determined by the difference
of the to
and from
vectors, defines the projectile's velocity vector.
The pointer to the gameent
object that the projectile is credited to.
Projectiles are created by players, so to track accuracy and kills, projectiles
are always associated with a player object.
The index for the attack type the projectile embodies. This attack type corresponds to a particular weapon's attack, and therefore serves as a proxy for the properties that particular weapon attacks have, such as the projectile's visible type, size, and damage. As a result, this value can only correspond to defined weapon attack types and encodes specific, specified sets of values.
This attribute flags the projectile as being local. Projectiles created by the player will have this flag set as true, and projectiles that are created as representations of other client's projectiles will have this set to false. This flag affects whether the game uses this projectile's location to determine hits (as other client's projectiles are not necessarily synced with the root client's machine, the game relies on those other clients to hit check their own projectiles. While this has some issues with abuse, this does mean that players who see their bullets hit a target will always be credited with those hits, even if other clients did not see the bullet hit them.
As the name indicates, this attribute sets the offset time for the projectile to start moving. This attribute is expressed in milliseconds, and needs to always be positive to have any meaning.
Each projectile is given a unique tracking id by the engine, which is set by the time at which the projectile spawns. This is the handle by which the projectile can later be identified (such as to find its owner).
Projectiles in the engine have simple kinematics, as neither gravity nor drag
act upon them. As such, projectiles move in straight lines at constant speed,
and do not drop or slow down over the course of their travel. This movement
continues until the projectile reaches its to
point defined in the object or
collides with something, after which point it is destroyed.
Every physics frame, the projectiles owned by the player's own client are checked for collision with players before being checked against the world geometry. Those projectiles that collide with players are then eligible to deal damage; projectiles that collide with world geometry are discarded after potentially throwing decals onto nearby surfaces (like burn marks or bullet holes).
As projectile speeds are defined in cubits per second, multiplication by 8 yields a speed in meters per second. As a result, for reference, the speed of sound at standard temperature and pressure (343 m/s) is equivalent to about 2750 cubits/s.
Since the trajectory of the projectile is parameterized in terms of its end location and its speed, the maximum time in flight must be calculated by the range by the speed.
Bouncers are entirely unrelated to jumppad entities and are the name given to the particles which can bounce off of surfaces. Bouncers do not interfere with players in any way, and are capable of bouncing off of geometry for an arbitrary number of times before being deleted and replaced with a decal.
Bouncers have two implemented types, gibs (bits of dead players) and debris, and
are defined by a set of twelve unique parameters as well as being a child of the
general physent
class of game objects.
Unlike projectiles, which are parameterized in terms of maximum range, bouncers are parameterized in terms of their lifetime. Note that while vectors like start position are not listed below, they do exist as part of the parent physent class.
The twelve unique parameters that bouncers have are as follows:
- int
lifetime
time left until the bouncer dies - int
bounces
number of bounces that have been done - float
lastyaw
yaw of the projectile at the last timestep - float
roll
roll of the projectile at the last timestep - bool
local
multiplayer sync flag for the projectile - gameent
*owner
gameent which the projectile belongs to - int
bouncetype
type of bounce the bouncer does - int
variant
random variant of bouncer that this bouncer has - vec
offset
world offset, starts at bouncer spawn point - int
offsetmillis
time of projectile creation - int
id
unique id assigned to each bouncer entity
Stains are a type of decal that is generally applied by the effect of another entity's death. Examples of this include the bullet holes left behind when a projectile makes contact with a surface or the blood stains left behind by a dead actor's giblet.
Stain objects have the following properties in their individual objects:
- int
millis
The game time when the stain spawns - bvec
color
The color of the stain texture - uchar
owner
The stain buffer the stain belongs to - ushort
startvert
The vertex in the buffer the stain starts at - ushort
endvert
The vertex in the buffer the stain ends at
Unlike typical particles and decals, stains, by virtue of their entirely cosmetic nature, have user-configurable settings to control their impact on visuals and performance. For this reason, stains should never be assumed as they can be turned off or modified client-side.
The commands:
stainfade <value>
time in s before stains fade awaystains <bool>
toggles rendering of stainsdbgstain <bool>
toggles debug output for stains
There are some Cubescript aliases which relate to shaders; those are not user commands and are not covered here.
Particles are billboarded objects which are rendered clientside and simulate small objects of various types. Particles broadly have three types: traditional pointlike particles, linear "tape"-like particles, and meters.
As billboards, particles always face towards the player and therefore are always viewed face-on. As a result, particle rendering is unique to each player and is not shared across the server (as a point will have different facing vectors to different actors' cameras.
Particles are not physents and do not bounce off of geometry nor interact with the world in any particular way. Particles do, however, cull themselves upon contact with geometry to prevent excessive resource usage.
Particles have many specific types which behave in different ways.
part
a point-like particle, such as puffs of smoke or flamestape
an unanimated line particle that extends between two pointstrail
text
a text billboard, such as a player's nametextup
meter
a meter with a fill between 0 and 100metervs
fireball
an animated fireball
- vec
o
origin vector triple - vec
d
direction vector triple - int
gravity
gravity scale (<0 for upwards floaters) - int
fade
fade scale - int
millis
time in ms before fade - bvec
color
color vector triple - uchar
flags
particle-specific flags - float
size
radius of particle - union
(char *text, float val, physent *owner, (uchar color2[3], progress)
The union type can be any of the objects listed inside the union. However, it can only be one object at once. The union structure is outlined below:
union
0 63 bit
+--------------------------------------------------------------+
| *text |
+--------------------------------------------------------------+
OR
0 31 63 bit
+-------------------------------+------------------------------+
| val ||||||||||||||||||||||||||||||||
+-------------------------------+------------------------------+
OR
0 63 bit
+--------------------------------------------------------------+
| *owner |
+--------------------------------------------------------------+
OR
0 7 15 23 31 63 bit
+-------+-------+-------+-------+------------------------------+
|color2 |color2 |color2 |-prog- ||||||||||||||||||||||||||||||||
+-------+-------+-------+-------+------------------------------+
The union is set as one of the four rows depending on the type of particle present:
*text
pointer for a text entityval
float for a fireball*owner
pointer for a particle's ownercolor2[3]
array andprogress
values for a meter
The pointlike particles are internally refered to as being of the type part
;
they are the particles that are most accurately refered to as a "particle".
Pointlike particles generally are created with some velocity along an axis and
additionally have some gravitational term which causes them to move in the z
direction.
Particle static entities of type water
, fire
, smoke
are rendered as
pointlike particles.
Tape particles are called tape
for their resemblance to barricade tape in its
stationary, straight appearance, and act to create beam-like effects in the
level. The static particle entity which uses tape particles also goes by the
name tape
; it creates tape particles along a certain direction.
Trail particles create a number of standard particles radiating out from a
region of space. The water
static particle entity type uses a particle trail.
Trail particles are potentially useful for following a projectile.
Text particles are most notably used ingame to render player names above their heads. They also make an appearance while editing entities, as the entity type is rendered as a particle above the entity origin.
Text particles have settable string, color, size and blend options to control the content, transparency, font size, and color of the rendered text. There font size is proportional to the particle's size parameter and does not follow typesetting convention (as these don't make much sense with a 3D engine).
Meters are a status particle used to show the size of a particular value passed
to its progress
value. progress
is capped to values up to 100, and as a
result meters are restricted to integral percents in their representation.
More resolution is not particularly important for these particles, as they do not display the actual value passed as a value, and those reading a meter particle ingame would have trouble discerning values within a percent.
Fireballs are animated billboards which appear as a large ball of bright gas. They are round and their general appearance is isotropic (no particular orientation). Fireballs pulsate and have their surface change with time, which makes them particularly suitable for relatively large sizes (normal particles are static and hence fairly boring if they are multiple meters across.)
Fireballs are perhaps the particle least obviously a 2d billboard, as a result its constant animation and scale change.
Physics apply to game entities called physents
. Physents have a large number
of properties which affect their time evolution, and additionally are able to
realistically collide with surfaces.
Physents include item drops, players, non-player actors, and bouncers. These entities also have additional properties unique to their respective entity types, as they are all seperate children of the physent class.
Physents all have the following properties:
- vec
o
origin vector (location displacement vector from origin) - vec
vel
velocity vector - vec
falling
gravity vector - vec
deltapos
position displacement - vec
newpos
interpolated next position - float
yaw
yaw angle (around horizon CW) - float
pitch
pitch angle (up/down; -90 down; 0 horizon; 90 up) - float
roll
roll angle (CW about yaw+pitch vector) - float
maxspeed
speed limit for this object (clamp speed to this level) - int
timeinair
time spent without being on the ground in ms - float
radius
size of entity - float
eyeheight
height of eyes (default player = 18(/8) = 2.25m) - float
maxheight
vertical size of entity (default palyer 18(/8) = 2.25m) - float
aboveeye
clearance above eyes (default player 2(/8) = .25m) - float
xradius
x hitbox radius - float
yradius
y hitbox radius - float
zmargin
z hitbox margin - vec
floor
orientation of floor below physent - int
inwater
material name of liquid that physent is in (0 otherwise) - bool
jumping
whether the ent is in the process of jumping - char
move
forwards and reverse movement - char
strafe
side to side movement - char
crouching
crouching (players) - uchar
physstate
type of behavior physent is undergoing (e.g. falling) - uchar
state
in normal play, state of physent (e.g. alive, spectating) - uchar
editstate
in edit, state of physent (e.g. alive, spectating) - uchar
type
type of entity (e.g. player) - uchar
collidetype
bounding box type (e.g. elliptical) - bool
blocked
toggles whether the ai should consider the physent blocked
The core of the Libprimis engine is its renderer. The renderer is what transforms the abstract objects in the world into visuals onscreen.
Libprimis' rendering capabilities are essentially a subset of Tesseract's, as many static features in the Tesseract engine are not possible in Libprimis' dynamic use case. The renderer is deferred, as opposed to forward as with engines like Cube 2, and is capable of large numbers of dynamic lights onscreen due to its architecture.
Textures on world geometry can have one or several shaders applied to it which affects its appearance. These effects are usually defined per-texture and therefore immutable ingame, and can be modified on a per-map basis.
This is the standard color image of the texture, and what the texture browser displays in its tiles. All other shaders also implicity present the diffuse map.
It is not possible (nor sensible) to have a texture without diffuse mapping, as it provides the base image upon which other shaders may take effect.
This is mapping the surface normals (the actual orientation of the surfaces) and calculating how the diffuse map's irradiated light changes because of the orientation of the texture (regions pointing away from you are going to have less area to shine light at you).
Normal mapping is a staple of 3D graphics, and nearly all surfaces have some normal direction variance (macroscopic variance). Only where surfaces are homogeneous and smooth (think lacquered surfaces) might it be applicable to forgo a normal map.
Normal maps have three channels, corresponding to the three components of the normal vector at any given point. By packing these three channels into a texture file, it's possible to encode normal vector information for an entire surface.
This creates a specular reflection (where the surface reflects light sources like a mirror) over the entire surface uniformly. The specular reflection borrows none of its color from the underlying texture, and all of it from the light it originally came from.
Specular highlights are homogeneous and do not take into account the reflectivity of the surface, which is taken to be a defined constant. Most real-world surfaces are not this homogeneous, and require a more complex shader, outlined immediately below.
This maps out certain areas of the texture to have more specular reflection than others: some parts of a texture, e.g. metal parts or areas worn smooth, are going to specularly reflect more than other parts of a texture.
A specular map is a single channel grayscale file encoding how reflective an area is for all locations on the surface.
- Note that bump is currently required to assign parallax to a texture, even though the two are not necessarily required to be together. There are very few circumstances in which it is possible to justify having parallax and not normal mapping.
Parallax mapping changes how visible parts of the texture are depending on the observers' position relative to them. This is different than normal mapping in that normal mapping merely reduces the intensity of light from regions facing away from you, while parallax mapping reduces its visible size. Together, parallax and normal mapping can create a fairly convincing substitute for actual geometry, though both have visible issues in their approximations at shallow angles.
A parallax map (also called a heightmap, as it encodes vertical position) is a single channel, which can be either its own grayscale file or the alpha channel of the normal map.
Triplanar mapping involves mapping the texture from three directions (x,y,z)
rather than one and using the information from those three orientations to allow
the texture to be mapped accurately at any orientation (rather than having
significant error at any orientation other than that of the cube face it
occupies). This is most useful for patching seams in compound curvature where no
patching of the seam with voffset
or vrotate
is possible.
Because triplanar mapping is fairly expensive, it is not recommended to be used
unless it is visibly needed. Triplanar mapping also disallows texture transforms
such as vrotate
, so it cannot be used where texture rotations are needed.
While standard triplanar mapping is useful for blending a texture with itself, blending a texture with another according to angle is possibly useful (maybe???) to smoothly transition without blendmaps.
The most plausible use case for triplanardetailworld
would be to blend three
textures together on rough terrain, using the two textures for triplanar detail
mapping and laying a blendmap over the top of it for the third. Otherwise, just
using blendmap is preferable unless under some very strict map size restrictions
or consistency of the blend with respect to angle is critical.
To add the triplanar detail shader to a texture, setting the shader to
triplanardetail*world
and declaring any normal/spec/etc. maps should be done
as usual.
The vslot to be blended should then be declared using texdetail <vslot>
along
with other texture commands (e.g. texscale
) at the end.
Glow mapping makes certain of the textures always be lit. This is typical for lights fixtures and computer equipment textures as well as other objects which are always lit.
The glow map is an intensity map of the areas for the engine to glow. It can be given in full RGB color for colored glow effects.
Note that this glow effect merely fixes the brightness of the texture to a specified level. It does not actually create a light entity or light nearby areas, which must be done with an actual light entity.
Libprimis' light and shadow system is built on a deferred rendering pipeline and is the main difference between it and older engines such as Cube 2. This deferred pipeline offers advantages largely in the quantity of lights that can be dynamically rendered onto the scene; however, it is not superior to Cube 2's forward rendering pipeline in all aspects. The Libprimis rendering pipeline is essentially the same as Tesseract's, and is outlined here.
There are essentially three types of lights in the engine:
- Sunlight
- Dynamic lights
- Static lights (map entities)
In the renderer, the latter two are treated in largely similar ways, while sunlight is in a privileged position in the engine, being the only source for which the engine's global illumination is enabled (due to performance issues).
The engine's renderer uses a texture called a shadow atlas to cache the mapping of lights onto surfaces in the game. The shadow atlas is a monochromatic texture stored the GPU, 4096x4096 in size (32Mib of VRAM), and contains the depth mappings of every light currently being rendered. The shadow atlas does not have strong protections limiting its occupancy and therefore excessive use of light entities can cause the shadow atlas to overflow and create visual artifacts.
The shadow atlas is a depth buffer which maps how far parts of the scene are from their sources. This is necessary for lights to determine how far away the things they are lighting are so as to facilitate appropriate light intensities.
The depth of the shadow atlas can be changed from its default depth of 16 bits
per pixel (16bpp) to 32 bits by changing the smdepthprec
variable. In general,
however, there is essentially no benefit to doubling the depth of the shadow
atlas to 32 bits.
The shadow map is the actual texture which gets applied to textures ingame. Using the depth information encoded in the shadow atlas, the shadow map contains brightness information for lights which are being rendered. Like the shadow atlas, the shadow map is square and monochromatic, but unlike the shadow atlas, the shadow map size is variable and can be adjusted from resolutions of 2^10 (1024x1024) to 2^14 (16384x16384), an increase of 256 times. This large range in shadow map size allows the engine's shadow map to scale in performance to accomodate both the high performance of modern dedicated GPUs as well as integrated graphics up to several years of age.
While the shadow map is monochromatic, the engine is indeed capable of lighting in color. Color is not required in the shadow map, however, as light sources are monochromatic; the final rendering output is modulated by the light entity's particular color from the monochromatic shadow map. In doing so, Tesseract's renderer saves the overhead of three channels per bit (or conversely, increases the allowable precision by three times).
The shadow map texture does not generally line up with shadow features, causing ugly zig-zag aliasing which is particularly noticible at low shadow map resolutions. To resolve this, the shadow map may be smoothed by different, increasingly finer and more resource intensive methods.
The simplest and cheapest method, used by smfilter 1
, is rotated grid
filtering. This filtering method is a simple antialiasing filter that reduces
"jaggies" in the shadowmap by attempting to alias them to a more beneficial
plane.
The two finer methods are increasingly wide weighted filters which can be fed
by either a simple bilinear tap method or by texture gathering. The latter is
the highest fidelity method, but the bilinear method benefits from native driver
support on modern GPUs. The size of the filter is either 3x3 for smfilter 2
or
5x5 for smfilter 3
; naturally, the wider filter is more compute intensive.
The use of bilinear taps or texture gather taps is controlled by the smgather
boolean variable.
Generally, however, shadow map filtering is a significantly faster way to remove ugly shadow map aliasing than shadow map resolution increases, though shadow filtering cannot construct sharper shadows like high shadow map resolutions are able to do.
The sunlight in Libprimis is provided by a cascaded shadow map for maximum performance while retaining high angular sharpness. The cascaded shadow map, which for the sunlight is simply planar (as the sunlight comes collimated from infinitely far away, there is no point in a 3d projection), consists of multiple shadow maps of the same orientation in consecutively larger sizes. The contents of a higher resolution shadow map (and its consequentially smaller angular size) are cut out of larger shadow maps.
The result of cascaded shadowmapping is that the sunlight map is increasingly higher resolution for regions closer to the camera location. This allows for relatively cheaper, lower resolution shadowmapping of faraway sunlit locations while maintaining good angular resolution up close.
In effect, cascaded shadow mapping is similar to mipmapping in its final form: lessening distant rendering load by utilization of low resolution textures.
Relevant CSM commands:
csmbias <value>
csmbias2 <value>
csmcull <boolean>
Toggles masking of smaller CSM within larger ones.csmdepthmargin <value>
csmdepthrange <value
csmfarplane <value>
Sets the size in cubits of the largest CSM.csminoq <boolean>
csmnearplane <value>
Sets the min size in cubits for the smallest CSM.csmmaxsize <pixels>
Sets the size CSM texs relative to the shadow atlas.csmpolyfactor <value>
csmpolyfactor2 <valie>
csmpolyoffset <value>
csmpolyoffset2 <value>
csmradiustweak <value>
csmshadowmap <boolean>
Toggles rendering of the CSM (& therefore sunlight)csmsplits <value>
Sets the number of CSM levels to use.csmsplitweight <value>
Bias towards splitting CSM close (high) or far (low).
Global illumination, also known as indirect lighting, is the illumination of surfaces by the light reflected off of other surfaces. Global illumination assumes diffuse reflection: that light that is shone upon surfaces bounces out at random directions at an equal rate. While this is not quite exactly true physically, it is a very good approximation to how lighting actually does diffusely reflect.
The global illumination in Libprimis is calculated via the Radiance Hints algorithm, which allows for a cheap approximation of indirect lighting via a collection of taps placed automatically by the engine in the level. These taps have light seeded by the values of a reflective shadow map (RSM) that is calculated from the global sunlight (and not on-map light entities). The taps then exchange with each other light "packets" which are then used to determine the brightness of the surrounding area. The light "packets" recieved are directional, so the taps store the light values in a low-order multipole expansion of spherical harmonics (the cheapest way to do so).
The use of global illumination is to ameliorate excessive point lighting (which is dynamic and therefore relatively expensive) by spreading the global sunlight around the level. As this global lighting is fairly inexpensive, levels should use sunlight + GI when possible.
The RSM, unlike the shadow map, stores its values in a total of six channels and two logical maps: the diffuse color of surfaces the sunlight impinges upon is necessary to determine the color of the light which is diffusely reflected off of those surfaces, as well as the orientation of those surfaces to determine how strongly light has hit those surfaces. No depth map is needed in this case: the sunlight, which comes from the far-field, does not meaningfully attenuate within the scale of the map.
The RSM is by default a quarter the resolution of the standard shadow map: the radiance hint taps generally don't have enough resolution to take advantage of a very sharp map, and the RSM requires six channels compared to the shadowmap's two.
Transparency, also known as alpha, applies to objects which are partially clear, but have some level of visibility, including with respect to other non-trivial shaders (like specular or parallax mapping). Transparency is used by glass material as well as by geometry which has had alpha material applied to it.
Transparency support in Libprimis is largely motivated by the particular rendering architecture included therein. As a deferred renderer, which composites full-scene maps of particular properties, Tesseract faces steep costs to having multiple rendering layers (rendering a surface blended with another surfaces). The compromise solution, while rather limited, does allow for limited (single-layer) transparency.
Transparent regions in the engine, as marked by alpha material for cube geometry, is rendered in a seperate step from ordinary geometry. The background geometry is rendered with the transparency removed as usual, and then following this the rendering pass is done over for transparent regions and then layered over the top.
Multiple transparency passes have rather poor performance, and as such arbitrary layers of transparency are rather non-performant due to the costs of making arbitrary numbers of transparency stencils followed by arbitrary numbers of rendering passes. Not only is the shading costs high for such an approach, but also the memory space and bandwith requirements for such an arrangement.
A limited form of two-layered transparency, however, is supported by the engine. The geometry that is flagged as alpha by the presence of alpha material can optionally have its backface (the face visible from the other side of the transparent region) rendered along with the front face. This requires extra graphics resources, however, as an additional transparency pass is required (though being on the backface of an already flagged region simplifies other facets of transparency), and generally is only recommended where it can emulate two seperate panes of reflective material.
This backface alpha property is enabled whenever the texture slot's alpha
property is set to a value greater than zero.
Screenspace posteffects are a family of methods which use raster buffers (essentially cached images) rather than the underlying geometry in order to do their effects. These effects are fairly inexpensive, owing to the fact that GPUs are very good at manipulating raster images, and as such they can approximate techniques that are otherwise impractical to implement.
Screenspace reflection may be the most well-known reflection technique as well as the most well-known screenspace effect, as it allows for a relatively cheap rendition of a notoriously difficult problem in game engine design: realtime, responsive reflections. Screenspace reflections, indeed, is by far the fastest and cheapest realtime reflection method, but it has traps and idiosyncracies that need to be made aware to artists and level designers.
Screenspace reflection works by mapping the output frame onto itself, taking a nearly-rendered frame and mapping this already-rendered frame onto reflected surfaces on the level. This is vastly cheaper than doing a full reflection pass, but at a cost: the only information the SSR pipeline has is that which is located in the field of view. Looking into a mirror, for example, can't be done with SSR, as the camera's location is not located at the location of the mirror but rather that of the player, and of course, the player can't see their own face through their own eyes.
Screenspace reflection, then, is mostly useful only for glancing views of reflective surfaces, which is usually adequate for non-glaringly reflective surfaces: the Fresnel effect in physics phys limits the reflectivity of most real-world non-metallic surfaces to only occur at glancing angles.
SSR should usually only be used, then, for non-metallic surfaces which will nearly always have their reflections mapped on the screen space itself: floors and other horizontal surfaces in particular are excellent for this.
In spite of these drawbacks, however, SSR is generally the cheapest reasonable way to provide the general appearance of reflectivity, and this is often good enough for casual users such as video gamers who are presumably concentrated on other things.
Currently, Libprimis only uses SSR for its water material's top surface, which satisfies nearly every condition where SSR is applicable: it's horizontal, non-metallic, and additionally its random movement distorts reflections, making extreme resolution less important.
A particular phenomenon of light propagation in real life is the darkening of corners by the lack of available space for light to propagate inwards from. A location in the corner created by two walls, for example, has a "field of view" of only a quarter sphere, versus the half sphere visible from a point located in the middle of a wall. Likewise, the junction of a ceiling/floor and two walls only allows 1/8th of a sphere's worth of light into its vicinity, darkening it further than a two-wall corner would.
These effects, in a perfect world, would be handled by high-resolution indirect lighting techniques that calculate the diffuse bounce off of all surfaces and compensate for this behavior. However, this is very expensive graphics-wise indeed, and as such a crude approximation is rendered in the screenspace.
This screenspace effect works by looking at the depth buffer, an image indicating how far objects on the screen are away from the camera, and darkening valleys found in the depth buffer. This is vastly cheaper than sampling geometry itself (as a depth buffer, as a single channel raster map, is very fast to manipulate) and therefore allows for the simulation of the darkness generated by the topology of the region. This darkening is then filtered to improve smoothness, creating a realtime darkening effect of corners ingame.
A screenspace effect only applicable to transparent surfaces, such as glass and alpha material, refraction emulates a particular effect that occurs when light travels through optically dense surfaces such as glass. At material boundries, light gets bent according to the surface normal of the boundary and the density of the material, manipulating otherwise collimated light and distorting it.
While refraction technically should also create a parallax effect when there is only one boundary (such as that which a pencil in a glass of water does), implementing this is a nontrivial effect and also not a typical use case (nearly all transparent volumes ingame are panes of some sort). As such, screenspace refraction assumes that the volume itself is of relatively trivial thickness, greatly simplifying the need for additional parallax calculations.
Screenspace refraction, then, distorts pixels in a region stenciled as transparent using the transparent surface's topside normal map, providing the impression that the imperfections in the material distorted the light coming through it.
Antialiasing is a means of overcoming particular artifacts common to nearly every type of raster graphics.
Raster graphics, by far the most common type of display rendering (used on essentially all computers, TVs, and handheld electronics) rely on a grid ("raster") on which color values can be placed. As this grid is (usually) square-sided, attempts to render lines which are not parallel to this grid become problematic, as there must be a skip pattern:
line of slope -1/10:
%%%%%%%%%%
%%%%%%%%%%
%%%%%%%%%%
%%%%%%%%%%
line of slope -1/4:
%%%%
%%%%
%%%%
%%%%
%%%%
%%%%
%%%%
%%%%
%%%%
%%%%
straight line:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
As we can see, the slanted lines placed onto the raster grid of text column are not particularly nice, and there's jumps every so often to approximate perfect angled lines.
The best thing we can do, then, is to create hints that blend together these discontinuities as nicely as we can manage with the grid, with less intense color in the surrounding highlights:
line of slope -1/10:
%%%%%%%%%%**
**%%%%%%%%%%**
**%%%%%%%%%%**
**%%%%%%%%%%
line of slope -1/4:
%%%%*
*%%%%*
*%%%%
*%%%%*
*%%%%*
*%%%%*
*%%%%*
*%%%%*
*%%%%*
*%%%%
straight line:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
The examples above, of course, are very crude, but smudging the fringes of edges like this is essentially the whole idea of antialiasing. Antialiasing, however, requires smoothing an area of many pixels (about two million a frame for a 1080p display) and therefore some serious hacks are needed to pump pixels very quickly.
Libprimis' implementation of screenspace antialiasing techniques, it should be noted, are mutually exclusive: only one can take effect at a time. Generally, however, there are limited benefits to stacking AA techniques (beyond MSAA), and so providing multiple screenspace AA techniques is not worth the effort.
SSAA is not implemented in Libprimis. It is described here as a useful basis for the more advanced antialiasing techniques.
Supersample antialiasing is perhaps the simplest antialiasing method to understand. SSAA simply renders everything at a higher resolution, and then is able to average down this high-resolution output to the lower-res display. This method is "perfect" in that no parts of the scene get missed by SSAA pass (of course, everything gets scaled up in view), but it is very expensive, especially for a deferred renderer which buffers the output scene in many images that get composited together; to use SSAA 4x on a 1080p screen is as expensive as rendering 4k native.
For this reason, the "naive" SSAA method is very seldom used for realtime graphics applications because it is simply too slow for the antialiasing it is capable of doing. Generally, cranking other methods of AA up to the same performance cost as a given SSAA level generate much smoother results, as they can bring more antialiasing power to bear on the areas of the scene that are actually jaggy.
In particular, SSAA implementations tend to be very intensive on the memory subsystem of graphics cards: working with huge buffers at ~4k resolution requires a lot of shuttling of graphics information between the cache and VRAM of a graphics card.
Multisample antialiasing is a significantly more useful AA technique than SSAA, but it suffers from many of the same issues that SSAA does. MSAA works by smoothing only the boundaries of fragment shaders, i.e. the edges of geometry in the level. In doing so, it can skip past smoothing texture faces which are usually not so abrupt as those created by geometry discontinuities (particularly those boundaries where one face ends and some other geometry is farther away, such as the outline of a foreground chair against a back wall).
This is still quite expensive however, especially in a deferred renderer which presents many different images which have yet to be composited to a final output image. This then dictates that MSAA implementations have to do a MSAA pass over every buffer, of which there are several, in order to properly antialias the scene; this then makes MSAA several times slower in a game like Libprimis than a forward-rendered game like Quake III or Cube 2.
Like SSAA, MSAA tends to have large memory bandwidth demands relative to screenspace antialiasing methods.
For this reason, while MSAA is included in Libprimis, it rapidly becomes prohibitively expensive at high levels. For high performance antialiasing in a deferred engine like Libprimis, a different technique is needed: screenspace antialiasing.
Fast approximate antialiasing is, as its helpfully honest name implies, a way to quickly approximate nicer forms of antialiasing such as MSAA. To do this, FXAA discards the edge detection by individual fragment shader on the back end that MSAA uses and instead opts to sample only the (nearly) final output image.
FXAA uses a shader-powered filter instead to calculate what parts of the scene are the most "sharp" and then uses a blur filter to smooth them out. This works very well indeed to smooth out the sharp parts of the output image. However, as FXAA has no knowledge of the background geometry which created the scene, it has to guess at what parts are problematically sharp. This causes the FXAA method to tend to oversmooth large parts of the scene that were supposed to remain sharp, leading to FXAA be considered a "grease filter" shader that blots out sharpness from the scene at large.
Unlike SSAA/MSAA however, FXAA is quite cheap with respect to memory bandwidth, instead utilizing the math units in a GPU more heavily to do its calculations. Usually, this ends up being cheaper than MSAA/SSAA, especially for a deferred engine like Libprimis, but it is worth noting that screenspace methods like FXAA do utilize a different part of the GPU heavily than MSAA/SSAA.
A rather clever form of screenspace antialiasing, TQAA borrows the information from the previous frame to use as data to antialias the scene much as SSAA does, but because it borrows information that already had its rendering costs sunk, it is much faster than a comparable SSAA 2x implementation.
The "quincunx" in TQAA, refering to a cross shape, is appropriate in that the previous frame is overlaid offset by half a pixel in each dimension:
Here the recycled frame "y" is laid offset to the current frame "x":
x x x x x
y y y y y
x x x x x
y y y y y
x x x x x
y y y y y
As one can see, the "y" pixels from the recycled frame have four nearby "x" neighbors from the current frame, creating a quincunx shape like those formed by the five pip side on a six-sided die.
Not all is perfect with TQAA, however, much as any other hasty approximation is. TQAA in particular is incapable of dealing with fast-moving and shape changing objects like players or projectiles, as they move fast enough relative to the frame rate that the TQAA method has trouble using stale information for antialiasing with. However, in general, TQAA resolves nicely antialiasing issues with static, predictable geometry, as levels tend to be mostly made up of.
TQAA, then, has to calculate what parts of an old frame it can map to a new one, eating up some shader resources in the process. In general, however, TQAA is quite cheap and decently effective for all but the most picky of eyes.
SMAA is the "crown jewel" antialiasing method included in Libprimis' parent engine, Tesseract, and it is generally the most effective method overall for antialiasing; it uses TQAA and MSAA in addition to its own method at higher levels for additional scalibility.
SMAA works by fitting parts of the screenspace area to particular common edge types ("morphologies"): (plain straight edges, curves, etc.) to treat specifically. This is not quite as simple as FXAA, as FXAA simply looks for big changes to smooth, and in return avoids the "grease filter" effect over all high contrast geometry that FXAA is known to exhibit. Doing so requires some shader power, like other screenspace antialiasing methods. In general, however, SMAA, particularly the base level without additional AA methods included, is superior to and roughly as expensive as FXAA.
Higher levels of SMAA utilize additional pixels stolen from TQAA and MSAA 2x in order to aid the filterer in sorting out interesting areas of the scene.
Generally, SMAA is the best general-purpose antialiasing method available in Libprimis and is generally recommended as the default; methods like high MSAA values are only particularly useful for promotional purposes (e.g. screenshots).
Actors are the entities that play the game: this includes human controlled players and bot controlled players. At this time, no support for nonplayer models exists: the only actors supported are ones that take the form of the player model.
Actors are enlarged humans with a height of 2.5m (8') and a breadth of about 1m (3' 3"). This slightly exaggerated size is such that a player can jump onto a 1m tall box without being too exaggerated. As a result, players can fit in 3m by 1m corridors without a problem, and crouch to fit in 2m by 1m corridors if necessary.
As the most intensive use of models is here in the creation of actors, this section also covers the technical implementation of the game's models.
Actors are objects ingame with a large number of properties kept which are synced to other players by the server ingame. This is significantly larger than that of conventional entities and is synced much more carefully, as they are the entities which are directly manipulated by players.
Actors are stored as an object gameent
which is the object synced to other
clients. The information kept for each actor is kept regardless of their
ownership by a human player or a bot AI: they both interact with the engine
in the same way.
Actors store the following properties in their object fields:
- int
weight
weight of the player for hitboxes - int
clientnum
the server ID for the player - int
privilege
the level of authentication for the player - int
lastupdate
time in ms since the last packet reception - int
plag
packet lag (time in ms between packets) - int
ping
ping time for client - int
lifesequence
- int
respawned
- int
suicided
- int
lastpain
- int
lastaction
- int
lastattack
- int
attacking
- int
lasttaunt
- int
lastpickup
- int
lastpickupmillis
- int
flagpickup
- int
frags
frags (kills) so far in the current game match - int
flags
flags captured so far in the current game match - int
deaths
deaths so far in the current game match - int
totaldamage
- int
totalshots
- int
edit
- float
deltayaw
change in yaw since last update - float
deltapitch
change in pitch since last update - float
deltaroll
change in roll since last update - float
newyaw
- float
newpitch
- float
newroll
- float
smoothmillis
- string
name
client name shown to other players - string
info
- int
team
1 (blue) or 2 (red): team the player belongs to - int
playermodel
index of player model - int
playercolor
index of player color (NOT hex color) - ai::aiinfo
ai
- int
ownernum
- int
lastnode
- vec
muzzle
and the following functions:
gameent
- void
respawn
~gameent
- void
hitpush
- void
startgame
Models in Libprimis are not especially first-class citizens compared to the octree geometry that the world is built on, but they still play an important role, particularly in player models.
Models for Libprimis currently are supported in two formats: OBJ (aka Wavefront) and MD5 (aka Doom 3). These two formats do not particularly overlap in their utility and a very terse overview of the formats is given here. Neither of these formats are unique to Cube and OBJ in particular is extremely widely used.
OBJ models are most useful for static, non-animated objects such as scenery, level assets, and other applications that do not rely on a complicated, feature intensive model format. OBJ files can store vertex data and UV mapping to allow accompanying texture files to be used to skin them. OBJ is a plaintext, easy to read (compared to other formats) format and therefore is very commonly used for basic models in all types of 3D graphics applications.
The MD5 format is the in-house format for the id 4 engine most notably used in Doom 3. It is a game-centric model format that allows for a skeleton to animate it (using pre-provided animation files) and therefore allows for animation of characters or other entities in the game. MD5 therefore is the model format of choice for the player model, as it requires animations to allow the player to move realistically ingame.
These are commands which are available to all formats, including static, non animated ones such as Wavefront (OBJ).
-
<fmt>skin [meshname] [tex] [masks]
-
<fmt>spec [tex] [scale]
-
<fmt>gloss [tex] [type]
type ranges 0..2 -
<fmt>glow [tex] [pct] [del] [pulse]
-
<fmt>alphatest [mesh] [cutoff]
-
<fmt>cullface [mesh] [cullface]
-
<fmt>color [mesh] [r] [g] [b]
-
<fmt>bumpmap [mesh] [tex]
-
<fmt>decal [mesh] [tex]
-
<fmt>fullbright [mesh] [bright]
-
<fmt>shader [mesh] [shader]
-
<fmt>scroll [mesh] [x] [y]
-
<fmt>noclip [mesh] [bool]
-
<fmt>settricollide [mesh]
-
<fmt>link [parent] [child] [tag] [x] [y] [z]
-
<fmt>load [model] [smooth]
-
<fmt>tag [tagname] [tx] [ty] [tz] [rx] [ry] [rz]
-
<fmt>pitch [scale] [offset] [min] [max]
-
<fmt>anim [anim] [frame] [range] [speed] [priority]
These are commands which are only available to animated model formats, of which the only current one is MD5.
<fmt>load [mesh] [skel] [smooth]
<fmt>tag [name] [tag] [tx] [ty] [tz] [rx] [ry] [rz]
<fmt>pitch [name] [target] [scale] [min] [max]
<fmt>pitchtarget [name] [anim] [offset] [min] [max]
<fmt>pitchcorrect [name] [target] [scale] [min] [max]
<fmt>hitzone [id] [mask]
<fmt>anim [anim] [animfile] [speed] [priority] [startoffset] [endoffset]
<fmt>animpart [maskstr]
<fmt>adjust [name] [yaw] [pitch] [tx] [ty] [tz]
The game is largely accessed by the user using various interfaces defined in the engine that power the game's user experience. This includes the menu system and heads-up display (HUD), as well as the somewhat lower-level (but still exposed to the player) console and scripting system.
Scripting in Libprimis is done using the language common to the entire Cube series of engines, Cubescript. Cubescript, at least the part that can be considered consistent across games, is a very simple language; however, it has very large numbers of commands which extend it and make it useful.
Cubescript itself has a simple set of semantics which defines how operations are carried out. There is essentially one type of object in Cubescript, the alias, and a small set of native operations to make control structures possible. Notably, and as a result of being a scripting language rather than a general purpose one, Cubescript's actual control operations are not explicitly codified into the language's semantics and instead are implemented as commands on the same tier as typical game modifying commands.
The []
chars define a block of code to be evaluated when necessary (at the
end of the parser's job, after other parts of the code have been resolved). This
means that []
chars are very useful for control commands, as they allow the
parser to defer execution of the material within them until necessary. These
blocks can be eliminated by the effects of commands (they can be entirely
ignored if the execution path does not run through them).
As expected for a delimiting pair of chars, the []
braces delimit the order
of operations: interior nested pairs of []
braces are executed before outside
ones. However, generally, disambiguation of the order of operations should be
done by the ()
parentheses defined below.
A notable side effect of the way brackets and parens work within the parser is that they must be defined inline with the command or assignment which they belong to. This means that while the following Cubescript is valid:
foo = [
"bar"
"baz"
]
this code cannot be parsed by the Cubescript parser.
foo =
[
"bar"
"baz"
]
Do note that these semantics are at odds with the C++ standards for the codebase, which adopt a style similar to the latter example.
The ()
parentheses act like the []
braces except that code within these are
executed early, such that their actions are carried out before parsing commands
outside them. This means that ()
parens are to be used for statements which
determine the behavior of control commands, as their result is relevant for
other parts of the code. These blocks of code are always executed, meaning that
()
chars are useless for control commands.
The =
equals sign, like in many languages, allows defining an alias, the
unit equivalent to a variable. Aliases can not only be used like variables but
also like functions, accepting arguments to a body (typically delineated by a
set of braces) by defining arguments within the body as the reserved alias names
$arg1 $arg2 ....$arg25
(there is a 25 argument limit in the parser).
foo = bar
The $
symbol causes the parser to look for a defined alias with the name
appended, allowing for the value of variables to be used by other functions.
The behavior of this is similar to unix's bash
shell language, while the
behavior is implicit in many other languages (Cubescript interprets symbols not
delineated by a $
at the beginning as a string literal).
foo = 1
bar = $foo //bar = 1
The @
symbol causes the value stored by an alias to be directly inserted where
it is called, from a scope determined by the nesting level. The @
symbol thus
allows for application in synthesis of compound alias names (by directly
depositing the value of a variable in an already-in-progress string, which is
then evaluated in full after the @
symbol has taken effect).
Note the difference between the $
lookup and the @
literal substitution:
foo = 1
bar = [
baz = $foo
]
foo = 2
bar //baz = 2
foo = 1
bar = [
baz = @foo
]
foo = 2
bar //baz = 1
Cubescript only supports inline comments (no comment blocks) using a pair of slashes. This can be done at the beginning of a line, or following the end of a written code line.
//I defined an alias `foo` here
foo = [
"bar"
"baz" //I defined `baz` here
]
Commands in Libprimis are the primary way to make Cubescript perform useful work (Cubescript is entirely useless without additional commands defined) as well as cause internal changes inside the engine. Commands are bound via a rather convoluted macro process, but the important part of how they work does not require fully understanding the technicalities of the way they are bound.
These commands are bound in a single self-contained macro, called ICOMMAND (for, unsurprisingly, InlineCOMMAND). This macro has the following arguments:
name
: the name of the command as called from the console or via Cubescriptnargs
: a string codifying the arguments the command accepts (see below)proto
: the C++ types that the arguments will pass to the bodyb
ody: the C++ logic body of the command
These commands comprise of two parts: the declaration of the command, and a
separate named function which is called upon the execution of the command.
It is slightly less flexible than the ICOMMAND
inline command macro in that it
requires that a function be created with the command's contents. If for some
reason the function and command cannot have the same name as its associated
function, it is possible to use the COMMANDN macro to define both the name of
the command and its related function separately.
This macro has the following arguments:
name
: the name of the function to be called as well as the command's namenargs
: a string codifying the arguments the command accepts (see below)
Multiple commands which reference the same function by name (e.g. COMMANDN) cannot be assigned, because the macros which define them cannot redefine the dummy variables used for their instantiation.
To return values to the interpreter, there are a handful of useful functions which allow this to be done.These functions should be set to "return" the desired value of a COMMAND or ICOMMAND's output (do not use the function's native return type).
intret
returns an integer to the interpreterfloatret
returns a float to the interpreterstringret
returns a string to the interpreter
Returned values from commands are not displayed directly to the console at
first, and must be manually displayed using echo
.
The nargs
part of the command macro is used to tell the command parser what
kind of arguments it should be interpreting. A single character corresponds to a
single argument and codifies its type; the number of characters indicates the
number of chars total.
S
s
: a string (char *) argumenti
: an int (int *) argumentb
: a boolean (bool *) argumentf
: a float (float *) argumentF
:E
T
a tagval (tagval *) objectt
e
an exec (uint *) coder
an id (ident *) object$
N
a number of args (int *) specifierD
an (int *) specifier for a cmd that checks for a key press (e.g. movement)C
a console (char *) argument to be printedV
1
2
3
4
For example, a command with nargs
equal to ssssif
would take four string
arguments followed by an integer and float argument; a command with nargs
equal to fffs
would take three floats followed by a string.
Variables, in the context of the Libprimis scripting system, refers to specific
ingame variables which have been provided to the scripting system by way of yet
another convoluted macro system. VAR
macros allow different types of variables
to be exposed to the scripting system in useful ways, meaning that options such
as settings can be directly controlled via Cubescript or the console.
Variables exposed to Cubescript which have the type of int
are the most common
ones used in Libprimis. These variables also take on the duty of boolean
settings, ones which are very common in the realm of settings, as Cubescript
carries no notion of a boolean.
A standard VAR
carries the following arguments:
name
The name of the variable, both to the engine and ingamecur
The value which the variable is initiated with at the start of the game*min
The minimum value that the variable is allowed to be set from ingamemax
The maximum value that the variable is allowed to be set from ingame
For floating-point numbers with decimals, it is possible to define accessible
variables with the FVAR
macro. This macro behaves nearly the same as the int
version of the macro, with the only notable difference being the replacement of
integer values by floating point ones.
A standard FVAR
carries the following arguments:
name
The name of the variable, both to the engine and ingamecur
The value which the variable is initiated with at the start of the game*min
The minimum value that the variable is allowed to be set from ingamemax
The maximum value that the variable is allowed to be set from ingame
For hexadecimal values, it is possible to define values as a HVAR
which makes
entry and display more straightforward. This type of variable is similar to an
integer variable internally otherwise.
A standard HVAR
carries the following arguments:
name
The name of the variable, both to the engine and ingamecur
The value which the variable is initiated with at the start of the game*min
The minimum value that the variable is allowed to be set from ingamemax
The maximum value that the variable is allowed to be set from ingame
For colors, it is convenient to save and use information using the widely used
six-digit hexadecimal HTML format (0xRRGGBB
), so a CVAR
macro is provided
to interface with these types of data. A CVAR
is implicitly limited to six
hex digits, naturally, but otherwise is not restricted and therefore lacks min
or max arguments like the numerical VAR
macros have.
A standard CVAR
carries the following arguments:
name
The name of the variable, both to the engine and ingamecur
The value which the variable is initiated with at the start of the game*
For the storage of strings, such as those used for e.g. a player's name, it is
possible to define accessible string variables with the SVAR
macro. This macro
only has two arguments, unsurprisingly, as there is no logical way to set limits
for its bounds like the numerical ones.
A standard SVAR
carries the following arguments:
name
The name of the variable, both to the engine and ingamecur
The value which the variable is initiated with at the start of the game*
*Note that the cur
value is always true at the very beginning of the game, but
can be changed ingame to differ from this value.
Persistent variables are variables which are saved to a configuration file at the time that the program is closed. This makes it useful for settings which are generally kept the same between runs of the game.
The *VARP
macros (VARP
FVARP
HVARP
CVARP
SVARP
) all behave the same
as normal variables except that the game makes sure to save them to a script to
be executed when the game is started again. These variables initialize their
cur
values to their defined values as normal, but rely on a startup script to
change them to their persistent states from the previous run.
A number of C++ objects are defined in the engine to facilitate manipulation in a replicable way. Many of these are geometry constructs which carry with them the algebra and geometry of the structures they describe.
The game also has many specific-purpose objects which are described in their particular section. This chapter is reserved for general, extensible objects with utility in many potential parts of the engine.
A large number of vector objects exist in the game to facilitate working with objects in 2D, 3D, 4D, quaternion, and dual quaternion vector spaces.
vec
is an incredibly ubiquitous object in the engine, where it is referenced
thousands of times over essentially every part of the game code. Key features of
the vec object:
vec
is always 3 dimensional, and has three defining float values.vec
has its three arguments as eitherx,y,z
orr,g,b
(in a union)vec
has many linear algebra operators defined for it: seeshared/geom.h
As the vec
object is only defined for a 3-vector, seperate classes like vec2
and vec4
are used to do two or four dimensional linear algebra. However, these
objects are much less common and also have less operators defined for it,
befitting a 3d engine where locations of objects in the world are nearly always
defined as a 3d vector.
An important note about the vec
object's many member functions is that they
are destructive: when you call a member function such as in the following
example, the state of vec a
is changed by the call to mul()
:
vec a(1,1,1);
float b = 2;
// `c` is (2,2,2), but `b` is ALSO (2,2,2) because mul() changes its state
vec c = a.mul(b);
To work around this, it is typical to see what appears to be an unnecessary
constructor call vec(vec T)
. This is to create a new tempoary object using the
constructor call which can have member functions applied to it without mangling
the original vector.
While this property of vec
's many member functions is most commonly seen in
vec
objects, the same behavior also applies to other related objects, such as
vec2
or ivec
.
bvec
is a 3d color vector object. As opposed to the standard vec
object,
which is useful mainly in world geometry, the bvec
vector is intended for use
in the color vector space. In this space, the three basis vectors are R/G/B.
bvec
does not inherit properties from the standard vec
object and is not
capable of doing standard linear algebra operations like vec
can; for this
reason, it is not suitable for use in standard geometric constructions.
Important properties of the bvec
object:
bvec
is always 3 dimensional, and has three defining character values.bvec
has its three arguments as eitherx,y,z
orr,g,b
(in a union)bvec
has mostly color conversion operators defined and few normal operators
Critically, values in a bvec
are of type char
, meaning they are one byte
long and can encode values between 0 and 255. There is no notion of sign with a
char
, and indeed having colors with negative values in its channels makes no
sense either.
Individual cube nodes, the heart of Libprimis' geometry system, are represented in the code by C++ objects. There are two main objects which define a cube in the level, which are described here.
The base object which defines a cube in the world. It has several attributes:
*children
A pointer to an array which contains child cubes within this cube*cubeext
A pointer to the extended info for the cubeunion: edges faces
A union type containing the cube's edge/face deformationtexture[6]
A six-entry array containing texture indices for each facematerial
The bitmask of the material filling this cubemerged
The bitmask of faces of the cubes which have been mergedunion: escaped visible
union of unmerged children nodes/visible faces
The extended information belonging to a cube.
*va
pointer to the vertex array which the cube belongs to*ents
pointer to entities within the bounds of the cubesurfaces[6]
vertex information for each of the cube's six facestjoints
list of cube t-jointsmaxverts
space allocated for vertices
This object contains the metadata needed for the engine to stitch cubes' faces together with other cubes into vertex arrays in a less geometry heavy fashion than naive cube rendering.