since you admittedly don't have much experience with 3D and (presumably) OpenGL, I'll give you a "bird's eye" overview of the process.
I'll do my considerations about OpenGL, but the basic reasoning yields for other APIs too.
When you render something with a modern version of OpenGL you create objects that will reside into the GPU memory, and then mostly operate on them by reference.
Let's say you want to create a cube with a simple diffuse texture, what you will do is:
- load the mesh for the cube (or other object) in your program [the mesh now resides in your "main" memory and can be directly accessed by your code, and you can do C/Python/whatever stuff on it]
- load the previously loaded mesh into the GPU (by creating a buffer object in OpenGL, and filling it with the mesh data). After this operation, you have 2 copies of your mesh: one in the CPU's memory, and one in the GPU's.
- release the memory associated with the mesh data in your CPU (very loosely speaking... pass me this ;-)), keeping only a reference (nothing more than an int!) of the mesh that still resides in the GPU memory [now you cannot "touch the data directly anymore, but you can tell OpenGL/DirectX what to do]
- repeat steps 1. 2. and 3. for the UV (and if required normal) coordinates.
A similar sequence is involved with the texture:
- load the texture data, e.g. the png/jpg file [disk -> CPU memory]
- create and load a texture in the GPU [CPU memory -> GPU memory]
- release the texture memory, and use the texture by reference using the id returned to you by OpenGL [GPU].
The interesting part is that from now you can instantiate how many textures/meshes you want without spending extra memory to hold each copy.
It's not very different from what people do in 2D all the time, just with one extra layer between the CPU and the screen: in 2D graphics (e.g. a game rendered with tiles), you load an image, keep it in your CPU memory and then just "copy and paste it" on the video memory by blitting it repeatedly.
Doing 3D graphic you have an extra step, where you load the mesh/texture/whatever in the GPU and get a pointer of sorts for them as a "receipt", before being able to render anything.
About the details of your question:
While I have some experience with simple 2D games, I am new to more > process-demanding 3D games.
OpenGL/DirectX and other 3D frameworks are quite often used for 2D games too, and as soon as you get a good grasp of them you'll see why :)
I understand that when we instantiate an object, we are saving memory because the instantiated copy does not have to save mesh memory (like vertices' position, UV cood and normals). So, the instantiated copy only needs to save a transformation matrix in order to properly position, scale and rotate the mesh structure they share with the original mesh.
100% true.
But can the instantiated copies have different texture or material from the original mesh from which they were instantiated?
Yes! Unity (and most likely UE) treats the data in a very granular way and allows:
- the same model to be instantiated with different materials
- the same material to be used on different models
- the same texture to be used on different materials
- etc...
and it does this, to mimic the actual memory layout in the GPUs: the "instantiated objects" are just very complex, very high level constructs based on the same basic bricks used by the GPU to store information, bricks which get used and reused as much as possible to limit memory consumption/transfer (and improve performances).
Even a material, for instance, is not a "basic object", in the sense that is actually an OpenGL shader coupled with some textures and other additional data, providing opportunities for more memory/performances optimization.
Therefore, when you instantiate 100 objects using the same model (same geometry), with 100 different 3D transforms (each object will be at a distinct position, and will likely have a unique orientation and scale) and one of two materials (i.e. red and blue, which is a realistic example for red/blue tanks in a "Risk!" game), you are actually using:
- a single shader
- a single copy of the same 3D model (vertices, normal, UV) as 3 buffer objects (arrays of data in the GPU...)
- 100 different modelview matrices (one for each tank).
- two textures
this is a very quick and cursory account, mind you, but it's still reasonably accurate, and yields for low level APIs, like OpenGL/DirectX and for "complex" frameworks/editors like Unity and UE (basically because the hardware they are running on, the GPU, is the same in both cases, and this leads to the existence of what can be seen as a loosely "right way" to program it).
Hope this helps!