A few summers ago I dove into the world of OpenGL, and quickly found myself way over my head! However, looking back, I ended up learning a lot more than I thought I did! Some of the concepts from learning OpenGL end up crossing over with the PVR, given how the functions are setup.
I'll attach the OpenGL tutorial I followed in the references below, in case anyone is interested!
What Is the PVR?
The PVR is the Dreamcast's built-in graphics chip. It handles all the heavy lifting when it comes to rendering. This is in contrast to software rendering (which people seem to love making tutorials about), which uses the CPU to render pixels on the screen and is much slower.There's a ton of documentation all over on the hardware, and how it functions, but very little on how to actually use it (aside from this tutorial, but quite frankly I was left a little confused afterwards).
The end goal of this tutorial will be to get something like the following on your screen using only the PVR! No OpenGL, no SDL, no software rendering!
Relevant Example Projects
- kos/examples/dreamcast/png/example.c
More or less what I'll be basing this tutorial off. - kos/examples/dreamcast/pvr
Literally anything in here will be relevant, but probably more advanced.
The Code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <kos.h> | |
KOS_INIT_FLAGS(INIT_DEFAULT); | |
bool running = true; | |
void Draw(){ | |
pvr_poly_cxt_t cxt; // A PVR Polygon context. This human-readable format is | |
// important for setting up our header, and by extension, | |
// our vertices. | |
pvr_poly_hdr_t hdr; // A PVR polygon header. The hardware equivalent of a | |
// rendering context. We submit this to the hardware. | |
pvr_vertex_t vert; // Our vertex that we'll be modifying and submitting. | |
pvr_poly_cxt_col(&cxt, PVR_LIST_OP_POLY); // We fill the polygon context with default parameters | |
// necessary to render the polygon in a list (PVR_LIST_OP_POLY) | |
// We use pvr_poly_cxt_col for untextured polygons | |
// and pvr_poly_cxt_txr for textured polygons. | |
// PVR_LIST_OP_POLY is a list of opaque primitives | |
// that we'll be submitting to the PVR to render. | |
pvr_poly_compile(&hdr, &cxt); // Then we compile the context into the header. | |
// This is necessary for rendering. | |
pvr_prim(&hdr, sizeof(hdr)); // We submit a primitive to the PVR. | |
// Not entirely sure why we submit the header first, | |
// but if I had to guess I'd say it's so the PVR knows | |
// how we want to render the following vertices. | |
vert.flags = PVR_CMD_VERTEX; // We flag our vertex as the start | |
vert.oargb = 0; // Vertex offset color. Not sure what this does, | |
// honestly. Haven't experimented with it. | |
vert.z = 1; // Our vertex's depth value. | |
// You can change this per-vertex, but | |
// we want them all to be at Z=1 for this | |
// example! | |
vert.u = 0.0f; // Our vertex's U and V values. These are | |
vert.v = 0.0f; // used in texturing the vertex. We won't | |
// be using these for this tutorial. | |
// This is our bottom-left vertex. | |
vert.argb = PVR_PACK_COLOR(1.0f, 1.0f, 0.0f, 0.0f); | |
vert.x = 0; | |
vert.y = 480; | |
pvr_prim(&vert,sizeof(vert)); // Submit it | |
// This is our top-middle vertex. | |
vert.argb = PVR_PACK_COLOR(1.0f,0.0f,1.0f,0.0f); | |
vert.x = 320; | |
vert.y = 0; | |
pvr_prim(&vert,sizeof(vert)); // Submit it | |
// This is our bottom-right vertex | |
vert.argb = PVR_PACK_COLOR(1.0f,0.0f,0.0f,1.0f); | |
vert.x = 640; | |
vert.y = 480; | |
vert.flags = PVR_CMD_VERTEX_EOL; // Tell the PVR this is the last vertex. | |
pvr_prim(&vert,sizeof(vert)); // Submit it. | |
} | |
int main(int argc, char** argv){ | |
// We setup our video mode. | |
vid_set_mode(DM_640x480, PM_RGB565); | |
// This sets up the PVR and initalizes the defaults. | |
pvr_init_defaults(); | |
while (running){ | |
pvr_wait_ready(); // We need to wait for the PVR to be ready to draw | |
// otherwise we'll crash. Learned that the hard way. | |
pvr_scene_begin(); // Tell the PVR our scene has begun | |
pvr_list_begin(PVR_LIST_OP_POLY); | |
// We render opaque objects here | |
Draw(); | |
pvr_list_finish(); | |
pvr_list_begin(PVR_LIST_TR_POLY); | |
// If we had anything that required transparency, it would be rendered here. | |
pvr_list_finish(); | |
pvr_scene_finish(); | |
} | |
return 0; | |
} |
Initialization
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// We setup our video mode. | |
vid_set_mode(DM_640x480, PM_RGB565); | |
// This sets up the PVR and initalizes the defaults. | |
pvr_init_defaults(); |
First, we have to initialize our PVR. We do this by first setting up the video mode, more details on that can be found here, and then we initialize the default values for the PVR. If we don't do this, nothing will show up on the screen!
The Render Loop
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
while (running){ | |
pvr_wait_ready(); // We need to wait for the PVR to be ready to draw | |
// otherwise we'll crash. Learned that the hard way. | |
pvr_scene_begin(); // Tell the PVR our scene has begun | |
pvr_list_begin(PVR_LIST_OP_POLY); | |
// We render opaque objects here | |
Draw(); | |
pvr_list_finish(); | |
pvr_list_begin(PVR_LIST_TR_POLY); | |
// If we had anything that required transparency, it would be rendered here. | |
pvr_list_finish(); | |
pvr_scene_finish(); | |
} |
Like most things in game development, we're going to want to draw our scene in a loop. Here we have our Draw() function sandwiched between a bunch of other nonsensical PVR-related functions. The comments should help, but the big idea here is that we're setting up a list of polygons to draw with pvr_list_begin, and then calling a function that handles the drawing of them.
I included the transparency list from the aforementioned PVR tutorial just to show where it'd go if you had anything that needed transparency.
The Draw Function
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pvr_poly_cxt_t cxt; // A PVR Polygon context. This human-readable format is | |
// important for setting up our header, and by extension, | |
// our vertices. | |
pvr_poly_hdr_t hdr; // A PVR polygon header. The hardware equivalent of a | |
// rendering context. We submit this to the hardware. | |
pvr_vertex_t vert; // Our vertex that we'll be modifying and submitting. | |
pvr_poly_cxt_col(&cxt, PVR_LIST_OP_POLY); // We fill the polygon context with default parameters | |
// necessary to render the polygon in a list (PVR_LIST_OP_POLY) | |
// We use pvr_poly_cxt_col for untextured polygons | |
// and pvr_poly_cxt_txr for textured polygons. | |
// PVR_LIST_OP_POLY is a list of opaque primitives | |
// that we'll be submitting to the PVR to render. | |
pvr_poly_compile(&hdr, &cxt); // Then we compile the context into the header. | |
// This is necessary for rendering. | |
pvr_prim(&hdr, sizeof(hdr)); // We submit a primitive to the PVR. | |
// Not entirely sure why we submit the header first, | |
// but if I had to guess I'd say it's so the PVR knows | |
// how we want to render the following vertices. |
Again, most of this is already commented, so I won't explain a ton. The idea is that we need a PVR header in order to actually render anything. We take our context, with our list of polygons to render, and compile it into a header that the PVR uses to render our vertices.
The Vertices
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is our bottom-left vertex. | |
vert.argb = PVR_PACK_COLOR(1.0f, 1.0f, 0.0f, 0.0f); | |
vert.x = 0; | |
vert.y = 480; |
Note: The aforementioned PVR tutorial makes a point to state that triangle vertices must be rendered in a clockwise fashion. Rendering them in any other order may result in issues.
We also have my favorite part: The color! Our vertices' colors (vert.argb) consist of an alpha value (transparency), followed by our red value, green value and finally blue value! We use PVR_PACK_COLOR to get our color values.
You can change these colors to whatever you want! I just went with the traditional red corner, green corner, and blue corner.
And that's pretty much it! We pack our vertices into primitives to render, the PVR runs through our list of vertices, and boom! Triangle!
A Few Notes
If you're like me, you probably tried to mess with the Z-values of the vertices to see if you could do something fancy. If you did, you'll probably notice you'll get something weird like this (where the bottom-right Z value is < 1)
Why is that? Isn't Z our depth? Well, it is, but this is showing us that our camera is rendering in orthographically, and not with perspective.
![]() |
An example of perspective vs orthographic projections. Author: Unknown |
If you trick your mind into believing that the red corner in the triangle above being really far away, it makes a little more sense.
I'm not quite sure how to get perspective working just yet, but I'm quite eager to find out! I'd really love to provide some solid tutorials on how to work with a camera in a scene, once I figure it out, myself!
References
- OpenGL Tutorial by SonarSystems
A good OpenGL tutorial, teaching you the basics of OpenGL and 3D graphics.
Hello! Thanks for the tutorials, they were very helpful, especially this one. I just wanted to share here that I found that pvr.h doesn't cover perspective transformations. In other words, programming PVR for now only in 2D, if you want 3D you have to use KGL or GLdc. This is reported in a comment at the beginning of the file. See for yourself, follow file link: http://gamedev.allusion.net/docs/kos-current/pvr_8h_source.html
ReplyDeleteAhh, interesting! Thanks for the info! I'll definitely keep that in mind.
DeleteI think the documentation might be indicating that the PVR itself doesn't have built-in functionality for doing transformations, but I believe you can still rely on third-party libraries to help! For instance, it also seems to mention that the PVR API doesn't support matrix transformations at all, but I seem to recall using the matrix3d.h library when I was messing around with the PVR... I'm not sure though! It'd be worth looking into!
In any case, I'd strongly recommend people to use GLdc now-a-days anyway! It's so much nicer to work with! :)
Yes friend, it sure is possible. I think I expressed myself badly, what I meant was that the PVR API doesn't support transformations. But no doubt you can get around this limitation by implementing the matrices or using third-party libraries as you said. By the way, this is a very interesting project. Let's see what we got! See you
ReplyDelete