Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3D Battles & OpenGL #66

Merged
merged 148 commits into from
Jan 17, 2022
Merged

3D Battles & OpenGL #66

merged 148 commits into from
Jan 17, 2022

Conversation

Kermalis
Copy link
Owner

@Kermalis Kermalis commented Aug 21, 2021

Done

  • General cleanup and optimization (usings and linq, swap to MathF)
  • Ditch SDL_Image in favor of a C# library
  • A few 3D battle backgrounds
  • Converted fonts to a shader and VBOs with GUIString (very clean and optimized)
  • Color, size, position, rectangle structs. Separated font colors out
  • Spinda
  • Removed the unused software render functions
  • Split MapLayout out of Map, and made it no longer use cache, since they could be changed
  • Cleaned up map/warp/obj stuff
  • Split the CameraObj rendering stuff to a MapRenderer class
  • Screenshot with F12
  • There is no more "LogicTick" and "RenderTick", they run on the same thread now
  • Make the battle use reset events
  • Updated PBE (floats for hp and weight)
  • Stop embedding resources. Keep them in the assets folder next to the build. This also keeps the model loading consistent, since it needs files next to other ones (Assimp)
  • Make Tilesets and their animations use a single texture atlas instead of several textures
  • ImageSheet as a texture atlas instead of multiple images. Remove all usages of "BitmapSheet" etc from AssetLoader
  • DayTint
  • Space Font atlas out
  • Battle Sprites & Trainer Sprite
  • Undo support for battle actions
  • Make fadecolor transitions their own shaders
  • Make battle spiral transition a shader
  • Sprite shadows
  • Triple color background
  • Convert Map Layout rendering to a VBO-based implementation #67
  • GUI shader
  • Battle camera movement
  • Obj shader

To Do

  • NADA

Wtf

This branch is ditching the current software rendering and instead switching to OpenGL. This means everything is drastically changing, but opens up the possibility for 3D and way cooler (and easier to implement) visual effects.

3D was possible in software rendering, and I actually accomplished it by still making it purely from the algorithms like the current renderer works. Everything was in-house with no libraries, and didn't even use the built in (System.Numerics) vector or matrix stuff. Learning all of that took a long time though, and I don't expect anyone using this engine to see that as a good thing. Also, it was very slow and limited in terms of what you could render. It was just models with textures, since it could no longer do textures AND a solid color, for example. Lighting was also super basic, being one vector pointing at the objects, which said how much light it should get.
Shaders are a way more powerful tool to have, and hardware rendering is way better in every way, so that's what made me decide to stop the "do everything yourself" approach behind all of the rendering algorithms. Doing clipping, depth buffering, and learning how to move textures with perspective... it's a lot to expect someone to follow.

So far, there's no downside to using OpenGL instead, aside from additional complexity and extra space needed to store the battle's 3d models. They aren't big though, and the additional functionality is well worth it, so RIP my hard work in software 3D. I put some images of what it looked like, so you can see how well it was implemented.

What I did to get to this point and why I didn't stick with the past solutions

2021-06-30_11-44-37.mp4

^ The way this worked was to have an inherited sprite class which just did math to place the sprite somewhere on the screen by giving it a Z coordinate. The balls are just ellipses being rendered based on their distance. So it wasn't 3D aside from having a Z coordinate. This didn't tell me how to make the battle background 3D or how to rotate the camera, how to have an FOV, etc. There was also an issue where the camera being too close would cause the game to slow down super hard, since it's trying to draw way more pixels (and once it got behind the camera, it'd try to draw infinitely more pixels)

2021-07-01_05-57-42

^ This was the first step of making an ACTUAL 3D engine. I had to make my own structs for vector3 and matrix4x4. I put the vertices into an array (making sure the order was always clockwise, so many assumptions of normals can be made) and started to do the work to actually project a 3D object with a projection matrix. Easy enough, but no textures, I can see the back sides, no lighting, and no culling or clipping. If the "camera" was close enough, infinite pixels would try to be drawn behind the camera, or to the side of it, which are not even visible, which was the same problem with the fake 3D. The camera doesn't exist yet; the cube is just translated by a few z units, and I was rotating it with a rotation matrix on the two axis. But the next step was to cull the sides you can't see.

2021-07-01_07-25-29

^ Alright, that was easy enough to do. I made one vector that represents a light coming in a certain direction. To check whether we can see the face at all, we do a dot product of that and the normal of each surface, which tells how similar the normal is to my light vector. Okay, so now we can just color the face based on the result of that dot product, which would give us some value that represents how much light is on it from our perspective. So far nothing is complicated, but it is about to be.

2021-07-01_07-46-56

^ Cool, nothing changed other than using a FillTriangle instead of a DrawTriangle. Also, I had to use some ambient lighting, because the faces would be unrealistically dark. So basically just Math.Max() the luminous amount.

So next we have to figure out how to clip everything outside of view of the camera, but that means we need to make the camera a thing. Before that though, I got excited about loading in .obj files, since the format was easy enough. It also made me do the depth buffer now so other faces are not in front of others (even if their dot product meant we could see it, that didn't account for it being behind something else)

2021-07-01_13-22-28.mp4

^ Okay, now I should really do the camera and clipping so I can move on to the good stuff. Once that's done, that's most of the complicated stuff out of the way. I can load levels and stuff since all of those triangles will be off-screen but not be rendered.

So the way a camera really works is you're moving the entire world around the camera. Since the camera is just your screen which is a fixed position. So we need to keep track of positions and then apply those as translation matrices to each model in the world. The HUD would still be rendered above everything because the software rendering allows me to do anything in any order. So I wasn't using quaternions yet because I hadn't learned about them, I was instead making rotation matrices and multiplying them with the view, and it took a while before it actually worked. It's just dumb math.

So the camera could move and rotate (except for roll which I figured out later after I was loading massive levels) but the game would still freeze up when I got too close to stuff. So now it's time to implement clipping. So I have to define a bunch of planes to clip stuff on. They would be the "render distance" outside of view, the four screen edges, and right in front of the camera. The render distance is already taken care of by the projection matrix, so it's just the others. I don't remember exactly the math used to make clipping work, but you would test whether any vertices of a triangle were on the outside of the plane, and then split the triangle by connecting the remaining vertices to form 1 or 2 new triangles. If the triangle wasn't outside of the plane, the entire triangle is just returned, so 1 triangle. Then I have to sort all of these triangles again and it's just chaos but that's how it fundamentally works.

2021-07-01_16-07-25.mp4

^ So that's how it looks. And to think your game is constantly doing that thousands of times without you knowing it, it's really cool to see how it works. By the way, the "wireframe" is just doing DrawTriangle after FillTriangle, like how I do in my GUIs. The colors were representing whether it was cut into 1 triangle or 2, I don't remember which color was which though. So now I can do the part I've been waiting for, which was the whole point, texturing. Now that's going to require that I redo how I get pixels from my bitmaps. So I just made it a callback called a "PixelSupplier".

Placing a texture onto a face is really hard, especially since the face is not exactly going to be a perfect rectangle, it'll be distorted. Having lighting affect it is easy though. But then you remember everything is triangles, so you have to somehow split the texture across two triangles even for a cube face, and then have it account for perspective (so it's not an affine map)
Lots of dumb math I don't remember, all I remember is it took a lot of refactoring and made me unable to simply have a model be colored. Since you need to store texture coordinates per triangle now, and separating two different types of triangles just so one can be colored and one can be textured, was stupid. So I removed the ability to simply color a model, which is something OpenGL can do easily with shaders. That was really the first sign of weakness in my eyes of making this from scratch.

So how do you actually texture the faces? Well like I said, each triangle has to now store texture coordinates. But this also has to apply for clipping. So finding where the new point is is actually quite easy, you find the point on the triangle that gets clipped and look at the two texture coordinates of the other vertices on that line, and put it somewhere between. That's the only easy part though.

Drawing a textured triangle was a little hard to figure out but made sense actually. Each triangle has to have a midpoint where the bend occurs. So either the top is flat or the bottom is flat. If neither are flat, then you just turn it into two triangles split at the midpoint, which now makes a top flat and bottom flat triangle. So now you can step along one line that's consistent between both triangles, step down one y at a time, and just find the x coordinates between each "line" of the triangle. Drawing the texture coordinates requires a bit of math that also takes into account the perspective of the camera, so dot products again. And so the vector3 has to become a vector4 actually, which you also have to do in shaders. But I didn't know that at the time, I found the code to just be very messy having a w component that's only used for this.

2021-07-02_05-02-43.mp4

^ Anyway it works, and I can slap any texture onto any face as long as there are texture coordinates provided. But the transparency doesn't work (because of the depth buffer), so I had to make it a threshold of how transparent it is, and whether it should be added to the depth buffer or not is based on that. Also, I didn't notice with that texture, but it was off, since texture coordinates specify the EXCLUSIVE bounds of the texture. So it was annoying to figure out how to exclude the exact values, while having texture wrapping. I'm just bad at math and didn't get it.

2021-07-02_22-26-29.mp4

^ Anyway, transparency first: You can see the eyebrows work properly, but you can see in his eye that the texture was slightly not mapped properly based on what I just said. Also I added the ability to make the camera move along in the direction it's looking, rather than just along an axis. That code is still there now since that's just how it works.
At this point I was loading any objects I could get (converting them to .obj with my super limited support of it) and trying them out, and they all worked except for some Mario 64 models which used weird materials and I didn't want to care about those. That was the second point of weakness I found. It doesn't really matter for just battle backgrounds, but there's so much that goes into models that what I had was literally the bare minimum possible. Loading vertices and texture coordinates. Now I started evaluating the whole thing, seeing how slow it was in CPU rendering if my PC wasn't as good as it was, seeing how the vector and matrix stuff I used wasn't hardware accelerated, etc.

2021-07-04_11-29-07.mp4

^ This is the last point I was at before I decided I would ditch all of this work, never to be seen again. I already looked at OpenGL at this point, and Assimp, which I'm using now.

Migrating from that to OpenGL

Finding a library for OpenGL bindings was way more work than it should be, but I settled on Silk.Net which also has Assimp bindings. I started out by commenting everything to do with the software renderer, and made the game use the same test gui I was using before, except all of that was deleted except for the camera stuff. I followed some GL tutorials to figure out how to render my same models, and it worked very quickly, being at the same point I was at before, already.

Now I decided to research the drawbacks of OpenGL, and wanted to try Vulkan, since I had already switched so many libraries and everything already, I didn't want to pour all this time into OpenGL to find out it would hold the project back. Vulkan was a huge pain, and seeing all of the stuff you have to do to do basic stuff, I said to myself that it wasn't worth it, especially with the point of the project being that anyone can learn to use it easily. Besides, OpenGL is very representative of what you'd do in an actual game studio, and moreso than software rendering, so it was a good choice in the end.

But the thing now is I have to convert EVERYTHING ELSE in the game to use GL, which took so many weeks. Getting the GUI to render at pixel specific coordinates, getting the screen resolution to not be the same resolution as the window, etc. All of it was extremely hard to figure out. But once I got that figured out, I started slowly converting everything.

So now I had a project full of errors for the next month at least, before I would see anything again. When I did get the bare minimum up (just the overworld and windows, no fonts or anything), it was running super slowly, since I'm calling a lot of stuff just to render one tile to the screen. Anyway I didn't care about that, I wanted to fix fonts. Fonts took at least a week on their own, and I settled with the GUIString stuff I have now. Then I slowly converted all the routines to use Pos2D and Rect2D, etc. Long story short, the migration took way longer than I thought it would.

This leads to the end of the "history" of where I am now. Some stuff still needs to be better utilized in OpenGL. Namely disposing the textures, and using VBOs for the maps. Probably should use shaders for all of that stuff. The only thing NOT ported yet is the DayTint, since that'd have to be a shader for the battle as well as the world. Oh, also the sprite shadows, but I think that'll be easy in a shader. But I guess I also need to add the sprites themselves to the battle, since right now there's nothing there. So yeah, some work needs to be done.

Well where is the stuff

Simply put, if I were to commit my whole journey through this (the software 3D rendering, trying out Vulkan, trying out GLFW, experimenting with the lights, etc) this branch would have hundreds if not thousands of commits. Yeah it's no joke how much I did to get to this point. I didn't commit once, and knew that it'd be horrible if I lost it all, but I didn't want to keep committing garbage for months, when most of it was unusable and just test stuff. Example below:

Stuff.mp4

^ Even now, I will not upload the test models you see there, since it's not needed.

So the commits you see in this branch are mostly NOT going to allow you to build, until I commit everything. I will commit one subject at a time which will help anyone read what is going on. So many files are changed by other ones for simple reasons, since I refactored everything a lot. If I wanted it to be buildable, I'd have to commit everything in one fat commit.

This is the first time a branch will have commits that are not each their own build.

…ader

This just removes the UI namespace, and makes more clear what each class is
This splits Map out
Tileset and Blockset are also moved to Render.World
WorldPos and Warp are new structs. Way better
Tileset and TileAnimation really need to be optimized for GL though
Obj is now IDisposable. This is primarily so the textures get removed when the obj is unloaded
The visual offsets are now Pos2D (which I didn't commit yet)
These are the main ways to use GL aside from calling directly
WriteableBitmap replaces the old Image, and they no longer expose the bitmap
I already committed SpindaSpots by accident, but here's the rest of it
This has shaders for drawing a rect or a texture to the screen. Would be great to have other shaders for other shapes
The transitions should be their own shaders also
A GUIString builds a vbo for the string which allows the StringPrinter's job to be easier. Also way more customizable in how the string is drawn. StringPrinter can now draw scaled strings
Way nicer
Forgot to commit SummaryGUI last time so here
No functional change if it were still plugged in
Here are the Mesh/Model classes, the importer, and the most basic shader for a model
The old camera code I wrote about is now PositionRotation
It applies to camera and models. It can be controlled in the debug build if you want. Its main use is to apply lerp/slerp to move around smoothly
It's a class instead of a struct because of the annoying requirement to set every field before exiting a constructor
The animator class helps interpolate between two
Point lights can be controlled to look however you want. The color can be brighter than 1.0 which will make it very bright
Adds the camera movement to the battle gui
@Kermalis Kermalis marked this pull request as ready for review January 17, 2022 01:34
@Kermalis Kermalis merged commit 2821f2d into trainer Jan 17, 2022
@Kermalis Kermalis deleted the 3d-battles branch January 17, 2022 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant