Particles becomes almost trivial once I decided to do it the easy way. The entire particle emitting and updating stuff is implemented in r_part.c file. All I need to rewrite is 3 functions:
D_StartParticles : where I clear the previous frame's particles in my particle container.
D_DrawParticle: where I append one particle to my particle container.
D_EndParticles: where I upload the particle container as a vertex buffer and issue a glDrawArrays(GL_POINTS, ...) call.
The shaders are also extremely easy. All the vertex shader needs to do is adjusting the
point sprite size gl_PointSize base on the distance of the particle to the view point. And the fragment shader simply copies the input color to to output.
The last missing feature is dynamic lights.
Dynamic lights sounds difficult but it really isn't. In fact I would even say it's one of the easiest features to implement.
All it takes is uploading the dynamic light positions to a uniform array (in R_PushDlights(), there are up to 32 dlights in Quake). Then in the fragment, go through all dlights, compute the light contribution with a dot product of the normal vector and light position, adjusted by the light fall off base on square of light distance to the current fragment. This is actually already quite a bit better than what the original Quake did in its software renderer, where it does the dlights on the low-res light maps and doesn't use the normal vector and doesn't compute the fall off.
Although the implementation is simple, the result looks quite nice: