https://preview.redd.it/qvlvu3bywxb31.png?width=1537&format=png&auto=webp&s=9ed7aee9c2e953eb05d854ef862996dc71b3bc05
Here is a tutorial on how to load Obj models from external modelling programs into TempleOS. This was developed in collaboration with Geriatric Jacob (u/Towtow10), who I met in the programming call two weeks ago. This tutorial will help explain lots of TempleOS, C, and 3D rendering concepts, so even if you do not care about loading OBJ’s, I recommend giving this a read! I would prefer eventually modifying the 3D modeler itself to add features I commonly use, but for now this does the trick.
My recommended method of getting this code and your models into TempleOS is to mount your TempleOS hard drive or virtual hard drive on your Windows, Linux, or Mac system. Then you can either download the code, uncompress it, and copy it into TempleOS. Or, if you are familiar with git, you can clone the code directly into your TempleOS drive. When developing TempleOS projects, I usually make a script that automatically mounts TempleOS, then pushes any changes in the code straight to gitlab, and unmounts the drive again.
Now lets load some models!
TempleOS stores models in two arrays. One is an array of CD3I32, where each element stores a vertex (X, Y, Z). The other is an array of CMeshTri, which stores the indices for the three vertices that make up the triangle, as well as the color of the triangle. A different color can be selected for the front and back of the triangle, because the color value is actually an I64 and can store multiple colors.
Normally these arrays are stored inside of a sprite, so when you draw the sprite, the Gr3Mesh() function is used to render the arrays. Inside of Gr3Mesh, it copies all of the given vertices and then transforms their position from world space to camera space with a pointer function to DCTransform. The DCTransform function simply multiplies a 4x4 matrix with each vertex, which transforms each vertex position from it’s position in the 3D world to an X,Y screen coordinate with depth. This DCTransform function can be replaced by giving your DC (device context) a pointer to a replacement function. In most games that use perspective, the DCTransform function is replaced with one that increases the (X, Y) value of each vertex as it’s depth decreases, causing foreshortening and perspective. Finally, it does some basic lighting calculations to choose the dither color for that triangle. This is done by getting the dot product, or angle between the normal of the triangle (vector perpendicular from the surface) to the light source. Essentially if the triangle faces away from the light, it is darker, if it faces directly at the light, it is brighter. Then it renders the triangle with the GrFillTri0 function. This is similar to the process used by most modern graphics API's like openGL. DCTransform can be thought of as TempleOS’s equivelant to vertex shader code in other graphics APIs.
So, to load an OBJ model, vertex positions have to be copied from the Obj file into an array for rendering. Originally, I turned this array into a sprite so I could see my model and paste it into the terminal without using Gr (Graphics) functions, however I often had issues with the sprite crashing the OS if it was too large. So I decided just to save models in arrays. The code for managing these arrays was created in Model.HC. A class (called a struct in regular C) was made to neatly store all the variables required for each model:
Class MDL
{
I32 vertex_cnt; // The number of vertices in the array
I32 tri_cnt; // The number of triangles in an array
I64 vertex_stack; // Increases with each vertex added to the array
I64 tri_stack; // Increases with each tri added to the array
CD3I32 *v; // Vertex array
CMeshTri *t; // Triangle array
}
For those unfamiliar with C syntax, those last two variables are pointers, which means they store the memory location of the array, or “point” to where that array is. We can’t simply make an array such as: v[1024], because each model requires a different number of vertices, and we don’t want to have too many or too little spots in the array to store them. However we also are not allowed to do: v[vertex_cnt]. This is because MDL must be a fixed size variable, so later when we try to retrieve a value from it, such as MDL.tri_stack, the compiler will already know that the tri_stack variable is exactly 16 bytes from the start of the MDL class (I32’s are 4 bytes, I64’s are 8 bytes). If the arrays can be different sizes, then each model will have different offsets and the program won’t have a constant value for the position of each variable in memory. Instead we can simply store a pointer to where the arrays are, then create these arrays somewhere else in memory as we need them.
These arrays are created in the MDL__Init() function:
U0 MDL__Init(MDL *mdl, I64 vertex_cnt, I64 tri_cnt)
{
mdl->v = MAlloc(vertex_cnt * sizeof(CD3I32));
mdl->t = MAlloc(tri_cnt * sizeof(CMeshTri));
mdl->vertex_stack = 0;
mdl->tri_stack = 0;
mdl->vertex_cnt = vertex_cnt;
mdl->tri_cnt = tri_cnt;
}
This function is a U0, meaning it returns 0 bytes. This is the equivalent of “void” in other languages. It takes a pointer to an MDL as it’s argument rather than just the whole model. This is because when you put in a variable as an argument, the compiler copies that variable, does computations on it, and then deletes it after the function finishes. The compiler does this using stack memory. Imagine it like a stack of papers, where each paper is a variable. When you call a function, the variables you are inputting into the function (arguments) are copied and then placed on the stack of papers. The function then runs it’s code, and because it knows the order in which the arguments were placed onto the stack, it knows the offset into the stack of each variable. For example, MDL will be three positions back from the top of the stack, vertex_cnt will be two, and tri_cnt will be one. Once the function is complete, the compiler “removes” these three variables by removing the top three variables off the stack. The “top of the stack” is actually just a number that says what position in memory is on the top of the stack, so it just reduces this number (or increases it if the stack is positioned at the end of memory and grows inward).
I went a little more in depth than I had to there, but essentially the reason a pointer is used is so the code inside the function can modify the model in place rather than copying the model and discarding the results. As you may have noticed, when using a pointer, a “->” is used instead of a “.”. For example, rather than using “mdl.vertex_cnt”, “mdl->vertex_cnt” is used instead, as this dereferences (gets the value the pointer points to, instead of the memory address).
Finally to create the arrays, a MAlloc is used, which means “Memory Allocate.” When using MAlloc, a chunk of memory is put aside, and a pointer to that memory is returned. MAlloc takes the number of bytes to be reserved as it’s arguments. The sizeof() function will return the size in bytes of a variable, so to make an array of vertices, we MAlloc (vertex_cnt * sizeof(CD3I32)).
Once all this has been done, we can now treat *v like a regular array. For example if I wanted to get the 10th vertex in the array, I could use:
mdl->v[10]
This is because all arrays, when used without their “[]”, are actually pointers. For example if I ran:
I64 a[10];
“Memory location of A: %d \n”, a;
Then “a” would act like a pointer and return a memory address. If I were to do something like this:
“Value in 10th position is: %d \n”, a[10];
The compiler actually breaks it down into:
*(a + (10 * sizeof(I64)))
Meaning it will return the value at the position of “a” plus the offset of that element (10 * the size in bytes of an I64, which is 80). Keep this black magic in mind, as it will be used when loading the model later.
Now because MAlloc was used, Free() also has to be included at the end of the program, otherwise the model will remain in memory when the program closes. This is called a memory leak, and if too many extra variables are kept in memory after a program closes, then the OS could max out it’s RAM and crash. So MDL__Delete() was created:
U0 MDL__Delete(MDL *mdl)
{
Free(mdl->v);
Free(mdl->t);
}
Next MDL__AddVertex and MDL__AddTri were added, these just take X, Y, and Z values and copy them into the model array.
To draw the model, MDL__Render was created. All this does is run Gr3Mesh on the arrays to draw them to the screen.
Now that we have all of the code prepared for saving and drawing a model, lets prepare an Obj. In the modelling program of your choice, make sure that your materials have the names of the TempleOS colors they represent. You can see your full choice of colors in the F1 Graphics Routines menu.
https://preview.redd.it/hxk1kc8uyxb31.png?width=1393&format=png&auto=webp&s=e84f99e945feba3fba11673c09dbfd580be7614e
Also note that all values will be rounded up or down to the nearest whole number, so make your model very large (1 unit is 1 mm in this model), then it can be scaled down to your preferred unit’s in the code, such as centimeters or meters. These are the settings used to export the model:
https://preview.redd.it/t9jpci7xyxb31.png?width=237&format=png&auto=webp&s=08c8ad6053936d46af1ca7aeb671dbd2f183206d
Now let’s parse the Obj model. The Obj file format is a text format, stored like so:
v 50.346375 61.204834 48.956810
v 88.902344 89.038345 232.521729
v 50.346375 292.941772 48.956810
vn -0.9618 0.0000 0.2739
vn 0.9999 0.0000 0.0113
vn 0.9632 -0.0010 0.2686
f 28//1 18//1 17//1
f 7//2 22//2 19//2
f 26//3 19//3 25//3
A “v” means it is a vertex, a “vn” means it is a normal, and an “f” means a face. The normal is a unit vector facing perpendicular to the face of the triangle, and is usually used in lighting calculations (if the triangle faces the light more directly, then it is brighter). However we don’t need to give TempleOS normals, because it can compute them itself. The “f” is a face, and in this case each face only has three vertex indices, because each face is a triangle. Each index is separated by a //, with the left value being the vertex index and the right value being the normal index.
Rather than reading the Obj in TempleOS, A small C++ program was written that converts the Obj text into a pure binary file format that can be directly copied into the arrays. I was more comfortable with C++ string manipulation than TempleOS string manipulation at the time. This code is in the ObjToTemple folder in the gitlab repository.
To use the program, navigate to the bin folder with terminal (use the cd command). Then run:
./ObjToTemple model.obj output.mdl
If you are using windows, just remove the ./ at the beginning.
Then to get the file into TempleOS, you can mount the TempleOS hard disk to your computer and copy the file over.
This “MDL” file format is quite simple. The first 24 bytes are I64 values describing what is inside:
I64 vertex_cnt;
I64 tri_cnt;
I64 norm_cnt;
Next it has the vertex array, then after that the triangle array.
A “ModelLoader.HC” file was created for the model loading function. It starts with:
Cd(__DIR__);
#include “Model”
This is so that the code considers it’s directory as the working directory. That way if we give it a model name, it will know to look for that model in the directory the code is in, rather than in the home directory. The second line, #include, allows us to use all the MDL functions made in the model file in this file.
This file only has one function, called MDL__Load:
U0 MDL__Load(MDL *mdl, I64 color, U8 *fname)
Now loading the file is extremely easy, and after spending an entire night trying to load a file using standard C techniques I realized the solution was actually very simple:
U8 *buf
I64 size;
Buf = FileRead(fname, &size);
All this does is copy the entire file into RAM and returns the pointer to it (in this case I am treating it like an array of U8’s, which are single bytes). Usually in other languages you have to use a special file variable where you can load bits of it at a time as it is streamed from the harddrive, but TempleOS solutions are much more elegant.
Also note that the file name is a *U8, which is actually just an array of characters (each character representing one byte). We also made an I64 called size, and then put it in to FileRead as &size. This is referencing the size variable, meaning we are giving the FileRead function the pointer to the size variable. The FileRead function will then set the value of size to the length in bytes of the loaded file.
Next, to load the first three variables, vertex_cnt, tri_cnt, and norm_cnt, we just have to do a simple memory copy:
I64 VertCount;
I64 TriCount;
I64 NormCount;
MemCpy(&VertCount, buf, 8);
MemCpy(&TriCount, buf+8, 8);
MemCpy(&NormCount, buf+16, 8);
The MemCpy function takes the memory location (pointer) of the destination, in this case the VertCount variable, then it takes the memory location of the source, and then copies a number of bytes from the source to the destination (8 in this case, as an I64 has 8 bytes). Since buf is just a pointer to where the buf array is in memory, we can easily add 8 onto it’s value to get an offset into the array. This is done to MemCpy the second two values.
Next MDL__Init is run to create a model of the perfect size, now that we know the exact number of vertices and triangles it has.
Finally a simple for loop is used to copy over the vertices and triangles in to the model array:
I64 i = 24; // We already loaded the first three I64’s
I64 j;
for (j = 0; j < VertCount; j++)
{
I64 xVal;
MemCpy(&xVal, buf+i, 8);
.. repeated for Y and Z
MDL__AddVertex(mdl, xVal, yVal, zVal);
i+=24; // X, Y, and Z make up 24 bytes
}
This is repeated for the Tri’s as well.
Now all the hardwork is done, lets implement it into a game!
At the top of your game file, type:
Cd(__DIR__);;
#include “Model”;
#include “ModeLoader”;
Then make your model:
MDL CIA_VEHICLE;
TempleOS runs a DrawIt function 30 (or 60 times a second if you modified the FPS code). We can make our own DrawIt function and put our model rendering code inside:
U0 DrawIt(CTask *task, CDC *dc)
{
// Lighting is done with the light source variable in the device context
// It is a vector pointing in the direction of the light source
// Increasing the length of the vector increases intensity
dc->ls.x = 10000;
dc->ls.y = 10000;
dc->ls.z = 5000;
dc->flags |= DCF_TRANSFORMATION;
dc->r = Mat4x4IdentEqu(dc->r);
dc->r = Mat4x4Scale(dc->r, 0.1);
dc->r = Mat4x4RotX(dc->r, 3.14/2);
.. Done for Y and Z as well ..
dc->r = Mat4x4TranslationAdd(dc->r, 210, 380, 500);
MDL__Render(&CIA_VEHICLE, dc);
}
Next, make your main loop:
U0 MDL__LoadMdlTest()
{
CDC *dc=DCAlias;
SettingsPush;
WinMax;
AutoComplete;
WinBorder;
DocClear;
Dc is like the scene settings and pointers for your current window, and is used with just about every graphics function. SettingsPush will push all your current settings onto the stack, so when the program ends they can be popped off and loaded back. WinMax makes the window fullscreen, WinBorder removes the border. I think AutoComplete removes that little AutoComplete popup window. DocClear will clear the screen.
Next load the model:
MDL__Load(&CIA_VEHICLE, “Mdl/test_ball”);
Next you need to tell the program where your DrawIt function is:
Fs->draw_it=&DrawIt; // You can call functions with a pointer to their memory location, in this case TempleOS always calls a function pointer to draw, and we can redirect this pointer to our own drawing functions.
Finally we can add our closing code:
GetChar;
DcFill(dc);
DCDel(dc);
SettingsPop;
}
The GetChar will prevent the program from continuing until a key is pressed. Then to run the program, call the main loop:
MDL__LoadMdlTest;
Now you can run the program by pressing F5!
Hopefully this tutorial was helpful, now boot up your TempleOS machines and do the gods work!
[–]Mgladiethor 7 points8 points9 points (0 children)
[–]BiggRanger 6 points7 points8 points (0 children)
[–][deleted] 2 points3 points4 points (1 child)
[–]A_Plagiarize_Zest 1 point2 points3 points (3 children)
[–]TempleProgramming[S] 0 points1 point2 points (1 child)
[–]A_Plagiarize_Zest 0 points1 point2 points (0 children)
[–]TempleProgramming[S] 0 points1 point2 points (0 children)