title | date | tags | |
---|---|---|---|
软光栅器MicroRenderer(二) 进入三维 |
2022-05-27 16:38:00 -0700 |
|
代码见:https://github.com/LJHG/MicroRenderer
本次主要讨论的内容是 mvp变换,三角形的裁切以及相机控制。
具体的mvp矩阵推导这里就不细讲了,关于mvp变换我基本上是参照了games101课程里的内容,可以看一下这里:Assignment1 · GitBook (ljhblog.top) 。其中我的view matrix 和 projection matrix 是完全按照课程上的描述实现的,不过最后的我的视图矩阵和透视矩阵好像实现出来好像都有点问题。。。所以目前我的这两个矩阵都是抄的别人的,暂时还没啥问题。
一旦你会计算mvp矩阵了,剩下的事情就很简单了,只需要去继承Shader然后实现一个新的类,并且把vertex shader重新实现为三维版本就可以了,在三维空间的shader里,vertex shader的职责就是把顶点进行mvp变换。
// 3d shader, implement mvp transformation for vertex shader, fragment shader just pass color
class ThreeDShader: public Shader{
public:
ThreeDShader(glm::mat4 _modelMatrix, glm::mat4 _viewMatrix, glm::mat4 _projectionMatrix);
VertexOutData vertexShader(VertexData &v) override;
glm::vec4 fragmentShader(VertexOutData &v) override;
};
VertexOutData ThreeDShader::vertexShader(VertexData &v) {
VertexOutData vod;
vod.position = glm::vec4(v.position,1.0f);
vod.position = projectionMatrix * viewMatrix * modelMatrix * vod.position; //mvp transformation
vod.color= v.color;
vod.normal = v.normal;
return vod;
}
glm::vec4 ThreeDShader::fragmentShader(VertexOutData &v) {
glm::vec4 result;
result = v.color;
return result;
}
因为不涉及具体的着色,所以这里我们的 fragment shader仍然传递颜色即可。
究极野路子,谨慎参考
关于三角形的裁切这个东西,一般来说应该是放在光栅化之前来做:渲染管线中,背面剔除和齐次空间的裁剪哪个先进行?为什么? - 知乎 (zhihu.com)。裁切有很多很复杂的算法。一般来说,大家会把三角形裁切和背面剔除放在一起来讨论。
不过我这里就比较拉了,为了实现最简单的方法,我把裁切放在了光栅化的时候来做。具体的原理就是,通过对经过了视口变换的坐标进行判断,也就是说如果x坐标不在 [0,width]
内,y坐标不在 [0,height]
内,z坐标不在[-1,1]
内,那么就不去画它。。。
具体的代码就是这样:
void ShadingPipeline::fillRasterization(VertexOutData &v1, VertexOutData &v2, VertexOutData &v3) {
// I don't know name for this algorithm
// ref: GAMES101 assignment2: https://www.ljhblog.top/CG/GAMES101/assignment2.html
float x1 = v1.position[0]; float y1 = v1.position[1]; float z1 = v1.position[2];
float x2 = v2.position[0]; float y2 = v2.position[1]; float z2 = v2.position[2];
float x3 = v3.position[0]; float y3 = v3.position[1]; float z3 = v3.position[2];
/**
* 关于clip是否应该在光栅化时做的问题
* 一般来说,clip好像应该在做光栅化前来做,但是如果直接在光栅化时做会十分方便...
* 这里我在光栅化时作了两个clip:
* 1. 一个判断z,如果z不在[-1,1]内,那么就说明不再视线范围内,直接不画
* 2. 另一个对left right bottom top 做一个裁剪,不去管屏幕空间外的像素了
*/
// clip1
if(z1<-1.0f || z1 >1.0f || z2<-1.0f || z2 > 1.0f || z3<-1.0f || z3>1.0f){
//out of depth range, clip
return;
}
// get bounding box of the triangle
int left = static_cast<int>(std::min(std::min(x1,x2),x3));
int bottom = static_cast<int>(std::min(std::min(y1,y2),y3));
int right = static_cast<int>(std::max(std::max(x1,x2),x3)) + 1; //ceil
int top = static_cast<int>(std::max(std::max(y1,y2),y3)) + 1; //ceil
// clip2
left = std::max(left,0);
bottom = std::max(bottom,0);
right = std::min(right,width-1); // need to minus 1, or will draw at the other edge...
top = std::min(top,height-1); // need to minus 1, or will draw at the other edge...
for(int x = left;x<=right;x++){
for(int y=bottom;y<=top;y++){
if(MathUtils::insideTriangle(static_cast<float>(x),static_cast<float>(y),x1,y1,x2,y2,x3,y3)){
VertexOutData tmp = MathUtils::barycentricLerp(x,y,v1,v2,v3);
int index = y*width+x;
if(tmp.position[2] < zBuffer[index]){
zBuffer[index] = tmp.position[2];
//fragment shader
glm::vec4 color = shader->fragmentShader(tmp);
int colorIndex = index*3; //multiply channel
image[colorIndex +0] = static_cast<int>(color[0]);
image[colorIndex +1] = static_cast<int>(color[1]);
image[colorIndex +2] = static_cast<int>(color[2]);
}
}
}
}
}
同时要注意的是,裁切非常的重要,不仅仅是性能的问题,还有显示的问题。如果你不对z方向进行裁切,会发生非常诡异的结果(明明不在视线内的东西还会画出来,所以其实最开始我是没发现要裁切的,直到我发现了这个诡异的bug)。
关于相机的控制,主要的实现流程就是控制相机,得到相机变换后的 位置,看的地方,然后直接算视图矩阵就行了。
这里要稍微注意一下,那就是在计算视图矩阵时:
glm::mat4 MathUtils::calViewMatrix(glm::vec3 cameraPos, glm::vec3 target, glm::vec3 worldUp)
这个 target
实际上是一个位置。一般来说,在创建相机时,我们需要一个变量来保存相机的朝向,例如:
class Camera {
public:
Camera(float _width, float _height);
glm::vec3 cameraPos; // camera position
glm::vec3 target; // camera focus position, target = cameraPos + cameraFront
glm::vec3 worldUp; // (0,1,0)
float fov;
float aspectRatio;
float zNear;
float zFar;
float width;
float height;
float cameraMoveSpeed;
float cameraTurnSpeed;
glm::vec3 cameraFront; // camera direction
};
由于相机看的地方肯定是在自己的朝向的前面,所以就可以使用 target = cameraPos + cameraFront
来计算相机看的地方了。
这部分具体的东西就是一些SDL2相关的操作了,贴下代码吧:
if(event.type == SDL_KEYDOWN){
switch (event.key.keysym.sym) {
case 'w':
{
camera.cameraPos += camera.cameraFront * camera.cameraMoveSpeed;
break;
}
case 'a':
{
camera.cameraPos += glm::normalize(glm::cross(camera.cameraFront, camera.worldUp)) * camera.cameraMoveSpeed;
break;
}
case 's':
{
camera.cameraPos -= camera.cameraFront * camera.cameraMoveSpeed;
break;
}
case 'd':
{
camera.cameraPos -= glm::normalize(glm::cross(camera.cameraFront, camera.worldUp)) * camera.cameraMoveSpeed;
break;
}
case SDLK_UP:
{
camera.cameraFront = glm::normalize(camera.cameraFront + camera.worldUp * camera.cameraTurnSpeed);
break;
}
case SDLK_DOWN:
{
camera.cameraFront = glm::normalize(camera.cameraFront - camera.worldUp * camera.cameraTurnSpeed);
break;
}
case SDLK_LEFT:
{
camera.cameraFront = glm::normalize(camera.cameraFront + glm::cross(camera.cameraFront, camera.worldUp) * camera.cameraTurnSpeed);
break;
}
case SDLK_RIGHT:
{
camera.cameraFront = glm::normalize(camera.cameraFront - glm::cross(camera.cameraFront, camera.worldUp) * camera.cameraTurnSpeed);
break;
}
}
camera.target = camera.cameraPos + camera.cameraFront;
}
if(event.type == SDL_MOUSEBUTTONDOWN){
mouseClick = true;
lastMouseX = event.button.x;
lastMouseY = event.button.y;
}
if(event.type == SDL_MOUSEBUTTONUP){
mouseClick = false;
}
if(mouseClick){
if(event.type == SDL_MOUSEMOTION){
glm::vec3 front = getCameraFront(camera,event.button.x,event.button.y);
camera.cameraFront = glm::normalize(camera.cameraFront + front * camera.cameraTurnSpeed * mouseDragSpeed);
camera.target = camera.cameraPos + camera.cameraFront;
}
}
最后就可以得到这样的效果: