I answer this question over and over on Reddit. So, here’s a wall of text for you :)

Here's the list of links I give everyone getting started in graphics programming in general.

The main thing you need to know is https://fgiesen.wordpress.com/2016/02/05/smart/

OpenGL is a good API to start with. There's a lot to learn regardless of which API you use. Once you can do an animated character in a scene with lighting, shadows, particles and basic full-screen post processing, you'll know how to proceed forward on your own from there.

https://learnopengl.com/
https://raytracing.github.io/
https://gamemath.com/book/
https://www.gameenginebook.com/
https://realtimerendering.com/
https://google.github.io/filament/Filament.md.html
https://fgiesen.wordpress.com/2011/07/09/a-trip-through-the-...
https://developer.nvidia.com/nsight-graphics
https://renderdoc.org/

A common mistake I see in beginners that you should definitely NOT do is to try to make completely self-contained classes like `tree.draw();`that attempt to set up and tear down all of the OpenGL state required to draw an object. The code required to make `tree.draw(); mainCharacter.draw(); rock.draw(); dog.draw();` work in random order is not only a very slow way to use GL, it is also extremely error-prone because the state set by an earlier object can accidentally affect a later object in unplanned ways.

Instead, it is much better to have all of the code for actually rendering a depth/shadow/static/animated/particle/UI pass contained in a function that handles 100% of the state setting for it's entire pass in a self-contained way. When you can look at the code all at once it becomes much easier to the straight in your head. It also makes it easier to set up in an efficient way.

Use https://realtimecollisiondetection.net/blog/?p=86 as a guide. Sort according to https://i.stack.imgur.com/JgrSc.jpg and you'll be doing better than most hobby engines.

[Modern Mobile Rendering @ HypeHype](https://enginearchitecture.org/2023.htm) describes a modern, high-end commercial implementation of the command buffer idea that started back in 2008 with the Order your graphics draw calls around! article. Obviously, you don’t have to be that advanced right out of the gate. But, it demonstrates a goal.

It's not a bad idea to have convenience classes for loading and specifying textures, shaders, meshes, and for packaging them up as a model. But, after loading those classes should not call more OpenGL functions until it is time to unload them.

Bonus points if you can load a large number of meshes into a small number of buffer objects, for loading asynchronously and for using glMultidrawElementsIndirect in your render pass loops.

Another common mistake is to make a scene graph with state modifiers. Like "Everything under this tree node is red plastic. Everything under this tree node is rippling". Horrible idea. Takes a huge amount of effort to semi-optimize.

A layout graph is fine. "The gun is attached to the hand of the character in the jeep on terrain segment 22 in sector[5,5]". With that you just need to figure out how to flatten the transforms quickly.

But, resolving arbitrary state permutations at runtime is fighting against the hardware and the driver.

Read through https://fgiesen.wordpress.com/2011/07/09/a-trip-through-the-... for more insight under the hood.