Homebrew [Tutorial] 3D OBJ Loader using LPP-3DS

PrintHello

Active Member
OP
Newcomer
Joined
Feb 10, 2016
Messages
37
Trophies
0
Age
28
XP
149
Country
Hey all, thought I'd make this for all you people out there with little coding experience but want to make something cool. :)

Also really sorry for the huge ass wall of text, and I'm 400% camera shy so this will have to do for now. Also big props to Rinnegaramante for making this possible.

This tutorial can also be found on my blog as well.

In this first of possibly many tutorials, you’ll learn how to write a 3DS homebrew app that can load .obj files and display them on the 3DS screen in 3D! If you are like me and have little to no experience in coding c++ (which is the main language for coding homebrew applications on the 3DS), don’t worry! We will be taking advantage of a lua interpreter called LPP-3DS by gbatemp user Rinnegatamante to make it a hell of a lot easier to write our very own applications for the 3DS.

For this tutorial I am on a O3DS with home menu version 10.4.0-29E using themehax with HBL version 1.1.0.

SECTIONS

1: Getting LPP-3DS and setting up your Lua IDE
2: Writing a base program
3: Creating the backbones of our render engine
4: Implementing the OBJ Loader
5: …
6: Profit?


#1 – GETTING LPP-3DS

AND

SETTING UP YOUR LUA IDE

There really isn’t much to setting up LPP-3DS. Head over to Rinnegatamante’s Github page to download the latest version of LPP-3DS. I used the nightly build from 1st Feb 2016 but if the latest is different and something breaks, please comment and I will fix up the tutorial.

Once you have downloaded the .3dsx and the .smdh files you can put them in a folder in the SDCARD:\3ds folder just like any other homebrew app. Simple as that.

For this tutorial I used SciTE which comes with the official Lua installer but notepad++ or even just regular notepad can be used.

#2 – WRITING A BASE PROGRAM

If you try and open LPP-3DS now it will greet you with a screen that says:

“Error: index.lua file not found”

The way lpp (I’m just going to call it lpp from now on) works is it finds and runs index.lua in its directory, and obviously we don’t have that yet, so we need to make it.

Go ahead and plug your SD card into your computer and open up your chosen IDE and save a blank file as index.lua in the same directory as your lpp file on your SD card.

Now we need to write out our base program to get the ball rolling. Here is what our program will look like:

Code:
Screen.enable3D() --Enabling 3D for the top screen
while(true) do

    Screen.waitVblankStart() -- Screen related stuff
    Screen.refresh() -- Other Screen related stuff
    Screen.clear(TOP_SCREEN) -- Clear top screen
    Screen.clear(BOTTOM_SCREEN) -- Clear bottom screen
    pad = Controls.read() -- Read Controls
    if(Controls.check(pad, KEY_START)) then-- check if start is pressed

        System.exit()-- Exit back to HBL

    end
    Screen.debugPrint(0,0,"Hello World",Color.new(255,255,255),BOTTOM_SCREEN) -- Print onto the bottom screen at (0,0), "Hello World" in white
    Screen.flip() -- More screen related stuff

end

As you can see this is a little program that writes “Hello World” to the bottom screen using lpp’s included Screen functions, and exits back to the HBL when we press START. There are many more functions and a whole list of them are on the documentation for lpp.

If you now save this as your index.lua file and put it in your 3DS and run LPP-3DS, you should now see “Hello World” written on the bottom screen. You can experiment with changing the color or changing what text is displayed depending on which button is pressed if you would like to help get the hang of it.

#3 – CREATING THE BACKBONES OF OUR RENDER ENGINE

So now we have our little program running on the 3DS, but it doesn’t really do that much at the moment.

Now the way lpp screen rendering works is you tell it to draw a line from (x1,y1) to (x2,y2) on the screen in a certain colour and (because we have 3D enabled) for a certain eye. Now when your eyes see things, the reason it looks 3D rather than flat is because our eyes are at slightly different points along the x axis (imagining x axis is flat on your face, y axis is directly up and down, and z axis is in and out of your face). Because however we only have access to 2D drawing methods (and thus no direct way to change the z position of objects), we need to create our own version to handle 3D rendering.

To create this faux sense of distance, points that are further away from the screen on the z axis look further apart when comparing right eye to left, than a point at z=0.

But before all of this, we need some way to store our points, which can then be used to draw lines between.

Here is the “class” we will be using to store our Vertex objects in:

Code:
Vertex = {};
function Vertex.new(x,y,z)
    local self = {}
    self.x = x
    self.y = y
    self.z = z
    return self
end

If you were to paste this at the top of your existing program (above the enable3D line), you now have a way of storing vertices. To create a set of vertices, simply add between this function and the enable3D function call:

Code:
vert1 = Vertex.new(0,0,0)
vert2 = Vertex.new(20,20,20)
vert3 = Vertex.new(50,50,50)

Calling vert1.x would return 0, vert2.y would return 20, and so on. Simple as that! Now to create our 3 dimensional drawing function. As i said above, objects that are further away look smaller, and are further apart compared to closer objects when we look at each eyes image side by side:

Code:
function round(num, idp)
  local mult = 10^(idp or 0)
  return math.floor(num * mult + 0.5) / mult
end

stereoMultiplier = 0.1
function drawPoint(x,y,z)
    Screen.drawPixel(round(x-z*stereoMultiplier ,0), round(y,0), Color.new(255,0,0), TOP_SCREEN, LEFT_EYE)
    Screen.drawPixel(round(x+z*stereoMultiplier ,0), round(y,0), Color.new(255,0,0), TOP_SCREEN, RIGHT_EYE)
end

Placing this snippet above our existing program gives us two important functions, the round function and the drawPoint function. The round function was copied and pasted from http://lua-users.org/wiki/SimpleRound and does what it says on the box. The drawPoint function takes an x,y,z and draws a point for each eye, moving the point on the left eye left and the right eye right depending on the z axis and this stereoMultiplier variable. This would normally be set to sync with the 3D slider, but for this tutorial a value of 0.1 was fine for me and the reason for the round function is that the Screen.drawPixel needs integer values (because we can’t draw a pixel half way between two pixels, it doesn’t make sense) so for now we use the round function.

And now we can draw 3D Pixels to the screen :D. Just add:

Code:
    drawPoint(vert1.x,vert1.y,vert1.z)
    drawPoint(vert2.x,vert2.y,vert2.z)
    drawPoint(vert3.x,vert3.y,vert3.z)

in between the Screen.debugPrint() and Screen.flip() functions in our main loop and save and run. You should (if your 3D slider is turned on) see three red pixels in 3D space.

Note: If in the drawPixel function the overall x or y values are smaller or greater than the screen can actually display, you will get an “out of bounds” error. For the moment just make sure that your vertices are inside the screen, we will fix this later.

If you don’t see anything, or your program is spitting out errors, here is a pastebin of our program up to this step. http://pastebin.com/N3T7qZTQ

But we’re still not done yet!

So far,we have created a simple lua program, and added our basic rendering methods to it for rendering vertices in 3d space. But objects are more than just vertices, they have edges too. To render lines, LPP-3DS includes the Screen.drawLine function, which is similar to the Screen.drawPixel function, but has two points instead of one, simple enough right?

To draw a line in 3d space, we need to add this to the top of our program:

Code:
function drawLine(vec1,vec2)

    Screen.drawLine(round(vec1.x-vec1.z*stereoMultiplier,0), round(vec2.x-vec2.z*stereoMultiplier,0), round(vec1.y,0),round(vec2.y,0), Color.new(255,255,255),TOP_SCREEN, LEFT_EYE)
    Screen.drawLine(round(vec1.x+vec1.z*stereoMultiplier,0), round(vec2.x+vec2.z*stereoMultiplier,0), round(vec1.y,0),round(vec2.y,0), Color.new(255,255,255),TOP_SCREEN, RIGHT_EYE)

end

And then place this in your main program loop:

Code:
drawLine(vert1,vert3)

And after running this, you should see a dandy white line in 3d between vert1 and vert3. :D

Easy right?

#4 - IMPLEMENTING THE OBJ LOADER

Now, one thing I haven’t figured out is including libraries while using lpp-3ds. So after editing the Lua OBJ Loader from Karai17 to use lpp’s file loading and other fiddly bits, we have a long piece of text we can paste into the top of our program, which will enable our program to load OBJ files.

So copy and paste this into the top of your program

Code:
-- OBJ LOADER LIBRARY --

local path ="."
local loader = {}
local fileString = ""

loader.version = "0.0.2"

function loader.load(file)
    local lines = {}
    fileStream = io.open(System.currentDirectory()..file,FREAD)
    size = io.size(fileStream)
    str = io.read(fileStream,0,size)
    io.close(fileStream)
     local x, a, b = 1;
      while x < string.len(str) do
        a, b = string.find(str, '.-\n', x);
        if not a then
            break;
        else
            if string.sub(str,a,b) then
                table.insert(lines,string.sub(str,a,b))
            end
        end;
        x = b + 1;
      end;
    return loader.parse(lines)
end

function loader.parse(object)
    local obj = {
        v    = {}, -- List of vertices - x, y, z, [w]=1.0
        vt    = {}, -- Texture coordinates - u, v, [w]=0
        vn    = {}, -- Normals - x, y, z
        vp    = {}, -- Parameter space vertices - u, [v], [w]
        f    = {}, -- Faces
    }
    for _, line in ipairs(object) do
        local l = string_split(line, "%s+")
        if l[1] == "v" then
            local v = {
                x = tonumber(l[2]),
                y = tonumber(l[3]),
                z = tonumber(l[4]),
                w = tonumber(l[5]) or 1.0
            }
            table.insert(obj.v, v)
        elseif l[1] == "vt" then
            local vt = {
                u = tonumber(l[2]),
                v = tonumber(l[3]),
                w = tonumber(l[4]) or 0
            }
            table.insert(obj.vt, vt)
        elseif l[1] == "vn" then
            local vn = {
                x = tonumber(l[2]),
                y = tonumber(l[3]),
                z = tonumber(l[4]),
            }
            table.insert(obj.vn, vn)
        elseif l[1] == "vp" then
            local vp = {
                u = tonumber(l[2]),
                v = tonumber(l[3]),
                w = tonumber(l[4]),
            }
            table.insert(obj.vp, vp)
        elseif l[1] == "f" then
            local f = {}
            for i=2, #l do
                local split = string_split(l[i], "/")
                local v = {}
                v.v = tonumber(split[1])
                if split[2] ~= "" then v.vt = tonumber(split[2]) end
                v.vn = tonumber(split[3])
                table.insert(f, v)
            end
            table.insert(obj.f, f)
        end
    end
    return obj
end

-- http://wiki.interfaceware.com/534.html
function string_split(s, d)
    local t = {}
    local i = 0
    local f
    local match = '(.-)' .. d .. '()'
    if string.find(s, d) == nil then
        return {s}
    end
    for sub, j in string.gmatch(s, match) do
        i = i + 1
        t[i] = sub
        f = j
    end
    if i ~= 0 then
        t[i+1] = string.sub(s, f)
    end
    return t
end

And now we have a way to load a .OBJ file into a variable. But, we still don’t have a way to convert the point cloud to lines. Now in OBJ models, each face is comprised of three points, and because some faces join at the same vertex, each vertex is stored in a list from 1 to the total vertices so there are not multiple entries of the same vertices, which saves on space. Each face contains the numbers on that list of which vertex is at each of its corners, ie. face 1 will have the vertices at position 5,7, and 12, and looking in the list of vertices, we can access the vertices at position 5, position 7, and position 12 in the vertex list, to find out the corresponding x, y, and z values.

And this is how we do it. (paste this somewhere in your program)

Code:
function renderAsTris(obj,pos,size)

    local xt = 1

    local n = 1
    while(obj.f[n]) do
    n = n + 1 --Loops through all of the faces and counts them
    end
    local numfaces = n

    while(xt < numfaces) do
        local vertex1ind = obj.f[xt][1]["v"] -- Gets the number in the list of the current vertex
        local vertex1 = Vertex.new(pos.x + size * obj.v[vertex1ind].x, pos.y + size * obj.v[vertex1ind].y, pos.z + size * obj.v[vertex1ind].z) -- creates vertex with position and size

        local vertex2ind = obj.f[xt][2]["v"]
        local vertex2 = Vertex.new(pos.x + size *  obj.v[vertex2ind].x, pos.y + size *  obj.v[vertex2ind].y, pos.z + size *  obj.v[vertex2ind].z)

        local vertex3ind = obj.f[xt][3]["v"]
        local vertex3 = Vertex.new(pos.x + size *  obj.v[vertex3ind].x, pos.y + size *  obj.v[vertex3ind].y, pos.z + size *  obj.v[vertex3ind].z)

        drawLine(vertex1,vertex2) --Draws the triangle
        drawLine(vertex2,vertex3)
        drawLine(vertex3,vertex1)

        xt = xt + 1

    end
end

This function takes the object variable (which we will make in a sec), a position vertex, and a size modifier, in case the object is very small.

Now to create an object variable, add:

Code:
local OBJModel = loader.load("/model file name here")

You need to put your model either in the folder that the index.lua is, or in a subfolder. If your model filename was “model.obj”, you would put loader.load(“/model.obj”) or loader.load(“/subfolder/model.obj”)(Remember the / part or else it won't load). You can do like I did and create a simple cube in Blender and export as an OBJ file, but make sure you select triangulate faces from the export menu or else your model will be missing half of its triangles.
Now, you can remove the

Code:
vert1 = Vertex.new(0,0,0)
vert2 = Vertex.new(20,20,20)
vert3 = Vertex.new(50,50,50)

and the

Code:
drawPoint(vert1.x,vert1.y,vert1.z)
drawPoint(vert2.x,vert2.y,vert2.z)
drawPoint(vert3.x,vert3.y,vert3.z)

drawLine(vert1,vert3)

Lines and add a new position vector somewhere near your main loop

Code:
objPos = Vertex.new(120,50,0)

And finally in your main loop, add this just above the Screen.flip() line to render your object:

Code:
renderAsTris(OBJModel,objPos,50)

Done! You can now load and display OBJ models in 3D on your 3DS :D.

If you are getting errors and you don’t know why, here is a copy of the completed file to check against http://pastebin.com/ADG21BRT (or if you can’t be bothered to do the tutorial)

Obviously this isn’t very optimised and displaying multiple high-poly models with movements and such will be very slow (esp. on the O3DS) so if anyone can find a way to make this faster please do let me know in the comments.

If you liked this tutorial, go check out my blog as I will be posting them there before posting them here (If you guys want more that is)

Thank you for reading this far, and good luck with your endevours :)

-PH
 
Last edited by PrintHello,

Rinnegatamante

Well-Known Member
Member
Joined
Nov 24, 2014
Messages
3,162
Trophies
2
Age
29
Location
Bologna
Website
rinnegatamante.it
XP
4,857
Country
Italy
This is really good!

Don't know if you checked latest commits of lpp-3ds in GitHub repository, i recently added real 3D engine which allows you to easily move your 3D objects and apply a vertex shader light effect, maybe you want to give it a try to adapt the code to this module: https://github.com/Rinnegatamante/lpp-3ds/blob/master/source/luaRender.cpp

A sample on its utilisation is this: http://pastebin.com/qgJa55Hq

The sample creates a textured cube which will move in the rendering scene each frame.
 
Last edited by Rinnegatamante,
  • Like
Reactions: I pwned U!

Stecker8

Plug
Member
Joined
Oct 9, 2015
Messages
526
Trophies
0
Age
32
Location
Here
Website
www.kernelhack10.3.com
XP
654
Country
G
This is really good!

Don't know if you checked latest commits of lpp-3ds in GitHub repository, i recently added real 3D engine which allows you to easily move your 3D objects, maybe you want to give it a try to adapt the code to this module: https://github.com/Rinnegatamante/lpp-3ds/blob/master/source/luaRender.cpp

A sample on its utilisation is this: http://pastebin.com/qgJa55Hq

The sample creates a textured cube which will move in the rendering scene each frame.
Great
 
D

Deleted User

Guest
copied and pasted the pastebin the OP uploaded into index.lua, tried it with the 1/2/2016 nightly, but it didn't work. :'(

Amazing concept, but I think I may stick to citro3D for the time being. lua hasn't been my greatest strongpoint in life. ;)
 

Rinnegatamante

Well-Known Member
Member
Joined
Nov 24, 2014
Messages
3,162
Trophies
2
Age
29
Location
Bologna
Website
rinnegatamante.it
XP
4,857
Country
Italy
  • Like
Reactions: Deleted User

PrintHello

Active Member
OP
Newcomer
Joined
Feb 10, 2016
Messages
37
Trophies
0
Age
28
XP
149
Country
Hey guys thanks for the love :)
And I will totally check out the new version w/ the proper 3D rendering stuff and do some edits.
I'll double check the pastebin but I'm fairly sure I copied and pasted it straight from my SD card so I dunno what happened there.
-PH

Edit: Tested the pastebin from the bottom of the OP and works fine with the R4 build. If you are getting an out of bounds error make sure your model is small enough to fit on the screen or else that will happen.
Edit 2: Haha I have no idea how to build a new version :/
 
Last edited by PrintHello,

Rinnegatamante

Well-Known Member
Member
Joined
Nov 24, 2014
Messages
3,162
Trophies
2
Age
29
Location
Bologna
Website
rinnegatamante.it
XP
4,857
Country
Italy
Hey guys thanks for the love :)
And I will totally check out the new version w/ the proper 3D rendering stuff and do some edits.
I'll double check the pastebin but I'm fairly sure I copied and pasted it straight from my SD card so I dunno what happened there.
-PH

Edit: Tested the pastebin from the bottom of the OP and works fine with the R4 build. If you are getting an out of bounds error make sure your model is small enough to fit on the screen or else that will happen.
Edit 2: Haha I have no idea how to build a new version :/

I can send you the latest commit already compiled if you want, send me a PM.
 

KialDaDial

New Member
Newbie
Joined
Feb 10, 2016
Messages
1
Trophies
0
Age
27
XP
52
Country
United States
Great tutorial, I worked my way through it the and one thing that may mess someone up is that the first time you state

drawPoint(vert1.x,vert1.y,vert1.z)

in the tutorial you put

drawPixel(vert1.x,vert1.y,vert1.z)

instead

Edit: took out "only" because I haven't checked the obj loader yet, just tested out the bottom script after a got a error for the drawPixel thing
 
Last edited by KialDaDial,

PrintHello

Active Member
OP
Newcomer
Joined
Feb 10, 2016
Messages
37
Trophies
0
Age
28
XP
149
Country
I can send you the latest commit already compiled if you want, send me a PM.

Nah it's all good, I'll wait for the official release. Got some non-3D centric ideas on the backburner.

Great tutorial, I worked my way through it the and one thing that may mess someone up is that the first time you state

drawPoint(vert1.x,vert1.y,vert1.z)

in the tutorial you put

drawPixel(vert1.x,vert1.y,vert1.z)

instead

Edit: took out "only" because I haven't checked the obj loader yet, just tested out the bottom script after a got a error for the drawPixel thing

Haha so I did. Thanks for catching that one, fixed it up now so it should read drawPoint when using the 3D space drawing function on everything.
 
Last edited by PrintHello,

Rinnegatamante

Well-Known Member
Member
Joined
Nov 24, 2014
Messages
3,162
Trophies
2
Age
29
Location
Bologna
Website
rinnegatamante.it
XP
4,857
Country
Italy
Nah it's all good, I'll wait for the official release. Got some non-3D centric ideas on the backburner.



Haha so I did. Thanks for catching that one, fixed it up now so it should read drawPoint when using the 3D space drawing function on everything.

Ok, good. Another little suggestion i can give you is to use Graphics module instead of Screen ones for drawing (Graphics module uses GPU, Screen ones uses CPU).
 

PrintHello

Active Member
OP
Newcomer
Joined
Feb 10, 2016
Messages
37
Trophies
0
Age
28
XP
149
Country
Ok, good. Another little suggestion i can give you is to use Graphics module instead of Screen ones for drawing (Graphics module uses GPU, Screen ones uses CPU).
Yeah I did have a go at that before but for simplicity I thought that I'd just stick to the screen rendering for now.
 

Site & Scene News

Popular threads in this forum

General chit-chat
Help Users
  • No one is chatting at the moment.
    K3Nv2 @ K3Nv2: It's mostly the ones that are just pictures and no instructions at all