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

Discussion in '3DS - Homebrew Development and Emulators' started by PrintHello, Feb 10, 2016.

  1. PrintHello
    OP

    PrintHello Member

    Newcomer
    37
    15
    Feb 10, 2016
    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, Feb 11, 2016
  2. Rinnegatamante

    Rinnegatamante GBAtemp Psycho!

    Member
    3,127
    3,270
    Nov 24, 2014
    Italy
    Bologna
    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, Feb 10, 2016
    I pwned U! likes this.
  3. Stecker8

    Stecker8 Plug

    Member
    461
    119
    Oct 9, 2015
    Here
    G
    Great
     
  4. Buttsnake

    Buttsnake "Mods please close this thread"

    Member
    207
    76
    Aug 23, 2015
    United States
    Inside your SD card
    This is absolutely amazing man. Once I get home I'm going to learn some shit today.
     
  5. Voxel

    Voxel Fable Junkie

    Member
    GBAtemp Patron
    Voxel is a Patron of GBAtemp and is helping us stay independent!

    Our Patreon
    5,199
    5,925
    Jun 27, 2015
    United Kingdom
    England, UK
    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. ;)
     
  6. Rinnegatamante

    Rinnegatamante GBAtemp Psycho!

    Member
    3,127
    3,270
    Nov 24, 2014
    Italy
    Bologna
    It obviously won't work if you see commits history: https://github.com/Rinnegatamante/lpp-3ds/commits/master
     
    Voxel likes this.
  7. Voxel

    Voxel Fable Junkie

    Member
    GBAtemp Patron
    Voxel is a Patron of GBAtemp and is helping us stay independent!

    Our Patreon
    5,199
    5,925
    Jun 27, 2015
    United Kingdom
    England, UK
  8. Rinnegatamante

    Rinnegatamante GBAtemp Psycho!

    Member
    3,127
    3,270
    Nov 24, 2014
    Italy
    Bologna
    As i previously said, latest commit of repository.
     
  9. Rinnegatamante

    Rinnegatamante GBAtemp Psycho!

    Member
    3,127
    3,270
    Nov 24, 2014
    Italy
    Bologna
    Looks like i misread what you wrote. For his pastebin, r4 should be good (also the nightly build should be ok).
     
  10. PrintHello
    OP

    PrintHello Member

    Newcomer
    37
    15
    Feb 10, 2016
    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, Feb 11, 2016
  11. Rinnegatamante

    Rinnegatamante GBAtemp Psycho!

    Member
    3,127
    3,270
    Nov 24, 2014
    Italy
    Bologna
    I can send you the latest commit already compiled if you want, send me a PM.
     
  12. KialDaDial

    KialDaDial Newbie

    Newcomer
    1
    0
    Feb 10, 2016
    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, Feb 11, 2016
  13. GalladeGuy

    GalladeGuy Freeze Kirby :3

    Member
    2,582
    2,641
    Oct 28, 2015
    United States
    This is great! I might try experimenting with this.
     
  14. PrintHello
    OP

    PrintHello Member

    Newcomer
    37
    15
    Feb 10, 2016
    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.
     
    Last edited by PrintHello, Feb 11, 2016
  15. Rinnegatamante

    Rinnegatamante GBAtemp Psycho!

    Member
    3,127
    3,270
    Nov 24, 2014
    Italy
    Bologna
    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).
     
  16. PrintHello
    OP

    PrintHello Member

    Newcomer
    37
    15
    Feb 10, 2016
    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.