I Made a 3D Engine in 1 Day (WebGL/JavaScript)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

yes! i was just listening to an old podcast i did on writing a 2d engine from scratch. i have been very curious about 3d and wondered how hard it would be. thank you for making this!

👍︎︎ 3 👤︎︎ u/tarley_apologizer 📅︎︎ Aug 27 2020 🗫︎ replies

Very cool man super informative in regards to the basics of a rendering engine!

👍︎︎ 1 👤︎︎ u/Joseph_Skycrest 📅︎︎ Aug 27 2020 🗫︎ replies
Captions
here's what's happening the goal is to have some sort of dinky little light pre-pass ranger up and running as quickly as possible i'll start the timer and try to bang this out over the course of a day now it's been about a million years since i was a real game programmer so i'll probably get a whole bunch of things wrong 3d engines at a high level aren't that complex they tend to have some nice abstractions around a few key concepts like you need to abstract away the state of the renderer there's a bunch of things that are reasonably common like having a camera so that we can easily view the scene from various angles a mesh model drawable type of thing representing objects in the scene a material or shader for representing things like how they should look these are all pretty common patterns then it needs to understand materials like what order to draw things that sort of stuff there's a bunch of crap i'm intentionally skipping like visibility system loading pretty much a million things but that's the basics the rough plan is get something on the screen start getting some of those parts working like the camera the mesh material try getting those parts working together and getting the various passes in in more technical terms this means that we need to initialize webgl get our abstractions working that would be like mesh shader camera get fbos working or frame buffer objects and then finally get the various stages of a light pre-pass renderer in place like zed pre-pass and normals light buffer generation and the forward opaque paths the first thing we need to do is start a new project so i'll just go make a new directory let's call this webgl one day 3d engine nice and straightforward name and we'll need a few files too so i'll just go ahead and make the index.html and main.js that we'll be working from so traditionally i've always used directx or other things never had much fondness for opengl in fact i find it really really hard to work with and 3js is a really good 3d library so i've never had much reason to use webgl directly the first thing we need to do is initialize webgl and we'll just start by searching for a tutorial i always find mozilla's docs to be super helpful so we'll head straight there looking over the code in here this looks pretty simple just a few lines of code and we should be up and running in c plus this would be way more code just because but in javascript it looks like it's just going to be a breeze in our code i've just created this basic renderer class that i'll be flashing out over time and i just copied the initialization code wholesale right into it and like the docs say once i run it i get a big black box so we're off to a good start maybe probably one thing to note is that i want to use webgl too and not have to deal with the crap ton of extensions and other bs that webgl1 has to deal with so in the code that just means we need to change this line here from webgl to webgl2 the first thing i want to do is get something on the screen so we're creating this mesh class to wrap the vertex data it's going to do just a few simple things you'll need to define some data let's say we'll make a cube so that we need to define a bunch of position data and we'll need to define some index data the mesh class will also need to send the data to opengl to upload to the gpu which means we need to create buffers for each data stream and then actually buffer the data you can see this function does exactly that creates a buffer and follows that with the buffer data call we do make a distinction here between normal vertex data and indices it's not perfect but it'll work don't really want to get too hung up on detail since i'm in a hurry lastly this needs to bind the buffers when we go to draw this thing so we have this bind function here and all it does is binds the various buffers that make up this 3d object in our case it's just positions and indices but later we'll include other data like normal and tangent data as well oh and we'll also need to draw can't really do it much without that so luckily it's just a couple lines we can't really see if this works yet because we need a material of some kind to assign there so let's get a shader material thing going first thing we need to do is go and create a shader class and it'll be a simple class with just a few basic responsibilities it needs to take the vertex and fragment source code it needs to compile them and then it needs to get some data back from the compiled source about where the buffers and uniforms are bound so we have these load calls which when you look inside the function definition they just take the source and type of program they create a shader send the source code then we compile it and this block of code here all it does is checks if everything succeeded or not and if it didn't it dumps it out on the console anyway that means that we'll need to define a simple shader too what we'll do here is just to find some colors for each side of the cube first so go back to the mesh class and define a few colors and then buffer them now in our shader here we don't need to do that much the vertex shader needs to transform the position which means we need some uniforms to define the world view and projection matrices we'll pass the vertex color via the varying attribute down to the fragment shader and we'll output the vertex color multiplied by the texture directly as the fragment output what this means is we'll need a way to bind the uniform values and supply them to the shader so this bind function in the shader will do just that it just sets the uniforms we need that also means we need to actually load a texture so let's also do that we'll create this quick texture class with a load function and this is almost copy pasted straight out of the mozilla docs we have a simple load function followed by some bind and unbind methods now let's add a shader to the mesh class so inside the bind function when we go to bind the mesh we also bind the shader that way when we're ready to draw the mesh we have all the specific shader data so now we draw and there we go cube there doesn't look like much so let's go back into code and make it spin a bit so you can see that it's a cube we'll add some quick methods to expose things like position and rotation to the mesh class here and now that just means that we need to add rotation to the model matrix we'll do that right here in the render function for now and when we load it up we have a spinny cube thing we're definitely making a lot of progress here even though it doesn't seem like we have anything up and this is all pretty trivial so we have a mesh and we have a shader now the problem is if i wanted to find a second cube with a second shader it's totally possible i could just write that right here in the renderer like this and it would work but remember the shader class also compiles the source and the mesh class creates a new set of buffers so this is all super wasteful what we want is to make a distinction between the source and an instance we'll create two new classes mesh instance and shader instance and what these will do is represent a specific instance of an object in the world think of it as the difference between a blueprint of an object and actual instances in the world with their own position rotation that kind of thing so we have these two new classes mesh instance and shader instance let's look at what they need to do mesh instance needs its own shader instance since it'll need to set its own uniform values it also needs to track position rotation scale with functions to be able to set those finally we need a way to set up the draw call so we need to create a bind function that sets up the various matrices and also binds the mesh data itself the shader instance class is also going to be slightly different from the shader class what it needs to do is make a copy of all the uniform values from the shader but the key difference here is that we'll also have the value that needs to be set so then in the bind function we can just loop over each of the uniforms and we can set each one nothing super complicated in here there's just one small gacha with the textures and that you need to manage the texture index yourself so we just keep a texture index variable here that gets incremented with every texture that gets bound so now back to where we wanted to define a second cube now we can do that easily by defining a couple of mesh instances and just repositioning them apart there we go got a couple of spinny cubes might seem like a lot of work for little gain again but you've got the bones in place for so much more now so we've got some things drawing we need to start building out the actual renderer the first thing we need to do is create some fbos or frame buffer objects these just seem to be opengl's way of supporting render targets but you know difficult to use because opengl so we'll go to find some fbos let's make this init g buffer function so we need to go and create that what you need to do is create a bunch of textures so we define the depth buffer normal texture here and the normal texture will be a floating point texture instead of the usual 8-bit rgba we'll make it the same width and height as the window and then down here we'll create a frame buffer and attach the various textures to it we'll need to make a slight change to the render function at this point now we need to use this bind frame buffer and draw buffers calls to set up the frame buffer and now we'll draw out all the normals for the scene and this didn't work i just got a black screen it's a bunch of spew in the console about frame buffer incomplete knew that my lack of error checking would come back to bite me anyway ended up googling around for a while and screwing around probably for an hour or two since i have no idea what frame buffer incomplete means other than i've screwed up something to do with the frame but for object creation obviously after consulting stack overflow and various other sources it turns out that you need to do a somewhat magical call to enable floating point textures with this line gl dot extension xt color buffer float after that things work fine i can write the normals to the buffer and bind and use that texture so to do lighting properly i need to reconstruct the world space position inside the fragment shader so i need a way to do that using the data from the zed pre-pass normal pass it's actually not super hard to recreate it from the depth buffer but i just wasted a bunch of time screwing around with getting just fbos working and i'm i wasn't confident about getting the depth reads working quickly so i opted for a second texture that just stored the positions so we go back to the fbo definition and we define a second floating point texture for world space positions and then we bind it to the frame buffer object now just a small change to the render function and we'll change the shader to also output position and i ended up with this it wasn't working again normals are working fine but the world space position isn't i thought this was going to be easier than getting the depth buffer reads working but right now it's not looking good i'll save you all the debugging steps but it turns out i just missed an s here so that was a total waste of time this is what it should look like the next step is generating the light buffer and in light pre-pass this is just a pass that takes in the normal and position and accumulates the lighting contribution in a new buffer so we loop over the lights and then we draw a screen space quad down here in the fragment shader is where the real work is done depending on which light we're using we read the normal position textures and then we calculate the lighting contribution at that point on the screen that's a simple n dot l for the diffuse and then the blinn fong specular uses the normal halfway vector raised to an exponent i just hard coded one here because i'm trying to get this done i'll come back around if i have time and since we're using just one buffer for the lighting the specular color isn't preserved and we only have the intensity in the w component once we do that we can output the light buffer to confirm it's all working and we've got some lighting on the scene adding more lights it's just a matter of blending them additively onto the buffer so if we enable blending and set the blend function to 1 1 then we can define a whole bunch of lights and suddenly the scene is full of lights the last step we need to go back and add one more pass this is the opaque pass where we actually draw the objects with their textures and whatever else this means going back and defining yet another frame buffer object and creating the textures that it needs these don't have to be floating point textures at this point and in fact we could just draw directly to the main frame buffer and not use an fbo but i'll use a frame buffer object with floating point textures because we could potentially tack on some post effects or tone mapping or something like that now it's a matter of looping through each mesh instance again and drawing them but last time we did this all the meshes are put normal in position and this time we need them to use a different shader which means we need each mesh to support having multiple shaders so we need to make some small changes around here we'll add this parameter to the bind function that lets you specify which pass you're doing then when we create a mesh instead of passing in a single shader instance we'll pass in a dictionary with multiple shader instances and it'll be keyed off the past name so here we create a mesh instance and it'll have a path called zed and that uses the zed shader and then it'll have another pass called color and that uses whatever shader was specified in the parameters this is what the color shader will look like it's a simple shader that samples the screen space light buffer and multiplies it against the diffuse texture now when we go render we specify what paths we're doing so the mesh knows which shader to bind and we have a whole bunch of spinny cubes up with lights moving around the scene randomly everything kind of looks wet because the specular but whatever i'm making the tech not the art at this point we're mostly done this isn't efficient and a whole lot of things could be done a zillion times better i ended up going back and instead of a full screen quad i switched this to add a set squad size for light and ended up burning the rest of my evening screwing around with this the whole point here is if you have a light on the screen you don't need to render the entire screen to get that light's influence you can just render a small quad so if you can figure out the screen space extents of that quad then you can just render a small quad and be done for this demo it really made no difference whatsoever but i thought i could squeeze it in but i couldn't i tried projecting the light center to screen space then from view space calculating up and right and using the radius and then getting a bounding box from that but i kept getting a cut off light so something was going wrong maybe i'd been sitting at my laptop way too long maybe it was because i was also drinking beer at this point but anyway finally found my error the next day my quad was only half size so i needed to multiply by two yeah that's where things ended up not gonna lie this is one steaming pile of code not the greatest but it works sort of and maybe if you wash this all the way through maybe you got something out of it source code is available like always go browse it on github i promise i remembered it upload this time look i even put it in the video as proof leave a comment tell me what you thought and what should i work on next cheers
Info
Channel: SimonDev
Views: 49,536
Rating: undefined out of 5
Keywords: simondev, game development, programming tutorial, webgl, webgl tutorial, javascript, javascript 3d, javascript 3d tutorial, 3d engine scratch
Id: Ms1vvo45Wi8
Channel Id: undefined
Length: 15min 5sec (905 seconds)
Published: Thu Aug 27 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.