C++模拟OpenGL库——图形光栅化理论及实现(三):三角形绘制

目录

判断点是否在三角形内

实现:区域扫描-遍历包围盒

优化1:平顶平底三角形扫描

实现:平底/平顶三角形绘制

实现:任意(非平顶/平底)三角形绘制

优化2:若三角形是否在屏幕范围内or屏幕是否在一个巨大的三角形内

优化3:对优化2中的情况进行剪裁

实现:绘制彩色三角形


判断点是否在三角形内

如何判断一个点在三角形中?

判断点p与pi是否在ei的同一侧

比如,看p点和p2这两个点。

判断此时p的x值,再计算此x值在e2边上的y值。

我们就知道了:p点的y值,和e2上的y值。

如果这p点y值和p2点y值都小于e2上的y值,则说明p和p2点在e2的同一侧。

其他两个点同理。都需要两两在对边的同一侧,即p点在三角形的同一侧,即在三角形内部。

实现:区域扫描-遍历包围盒

根据这个原理,来实现函数:

 

最直观的想法:遍历所有像素,判断点是否在三角形中,如果在则绘制。

但是太浪费了,做一个包围盒优化:

这样只需遍历包围盒即可,不用遍历整个屏幕

	void Canvas::drawTriange(Point p1, Point p2, Point p3){
		RGBA _color(255, 0, 0);

		//构建包围盒
		int left = MIN(p3.m_x, MIN(p1.m_x, p2.m_x));
		int top = MIN(p3.m_y, MIN(p1.m_y, p2.m_y));
		int right = MAX(p3.m_x, MAX(p1.m_x, p2.m_x));
		int bottom = MAX(p3.m_y, MAX(p1.m_y, p2.m_y));

		//裁剪屏幕
		left = left < 0 ?  0 : left;
		top = top < 0 ? 0 : top;
		right = right > (m_width - 1) ? m_width - 1 : right;
		bottom = bottom > (m_height - 1) ? m_height - 1 : bottom;

		//计算直线参数值
		float k1 = (float)(p2.m_y - p3.m_y) / (float)(p2.m_x - p3.m_x);
		float k2 = (float)(p1.m_y - p3.m_y) / (float)(p1.m_x - p3.m_x);
		float k3 = (float)(p2.m_y - p1.m_y) / (float)(p2.m_x - p1.m_x);

		//计算直线的b值
		float b1 = (float)p2.m_y - k1 * (float)p2.m_x;
		float b2 = (float)p3.m_y - k2 * (float)p3.m_x;
		float b3 = (float)p1.m_y - k3 * (float)p1.m_x;

		//循环判断
		for (int x = left; x <= right; x++) {
			for (int y = top; y <= bottom; y++) {
				//判断是否在当前点是否在三角形内
				float judge1 = (y - (k1 * x + b1)) * (p1.m_y - (k1 * p1.m_x + b1));
				float judge2 = (y - (k2 * x + b2)) * (p2.m_y - (k2 * p2.m_x + b2));
				float judge3 = (y - (k3 * x + b3)) * (p3.m_y - (k3 * p3.m_x + b3));

				if (judge1 >= 0 && judge2 >= 0 && judge3 >= 0) {
					drawPoint(x, y, _color);
				}
			}
		}


	}
void Render() {
    _canvas->clear();
    
    GT::Point p1(0, 0, GT::RGBA(255, 0, 0));
    GT::Point p2(100, 50, GT::RGBA(255, 0, 0));
    GT::Point p3(150, 200, GT::RGBA(255, 0, 0));

    _canvas->drawTriange(p1, p2, p3);

    //在这里画到设备上,hMem相当于缓冲区
    BitBlt(hDC, 0, 0, wWidth, wHeight, hMem, 0, 0, SRCCOPY);
}

优化1:平顶平底三角形扫描

直白来讲,就是将三角形分为若干个平顶/平底三角形

平顶/平底三角形绘制则可以通过之前的Brensenhem直线绘制来实现:

  • 找到平顶/平底三角形的”中间点“(比如上图pt),做一条垂直x轴的垂线
  • 这样就知道了两条边的y值,根据直线方程即可知道两条边对应的x值
  • 知道两个x值(x1,x2)就可以画出一条直线,从顶点一直画到底,即可扫描填充三角形

最后,需要考虑直角三角形,需要特殊判断点的x坐标。

实现:平底/平顶三角形绘制

	void Canvas::drawTriangeFlat(Point pFlat1, Point pFlat2, Point pt){
		//两边直线斜率
		float k1 = 0.0;
		float k2 = 0.0;

		if (pFlat1.m_x != pt.m_x) {
			k1 = (float)(pFlat1.m_y - pt.m_y) / (float)(pFlat1.m_x - pt.m_x);
		}
		if (pFlat2.m_x != pt.m_x) {
			k2 = (float)(pFlat2.m_y - pt.m_y) / (float)(pFlat2.m_x - pt.m_x);
		}

		//两直线b值
		float b1 = (float)pt.m_y - (float)pt.m_x * k1;
		float b2 = (float)pt.m_y - (float)pt.m_x * k2;

		//做垂线
		int yStart = MIN(pt.m_y, pFlat1.m_y);
		int yEnd = MAX(pt.m_y, pFlat1.m_y);
		for (int y = yStart; y <= yEnd; ++y) {
			int x1 = 0;
			//判断是否为直角三角形
			if (k1 == 0) {
				x1 = pFlat1.m_x;
			}
			else {
				x1 = ((float)y - b1) / k1;
			}

			int x2 = 0;
			//判断是否为直角三角形
			if (k2 == 0) {
				x2 = pFlat2.m_x;
			}
			else {
				x2 = ((float)y - b2) / k2;
			}

			//构建这两个点
			Point pt1(x1, y, RGBA(255, 0, 0));
			Point pt2(x2, y, RGBA(255, 0, 0));

			//绘制直线
			drawLine(pt1, pt2);
		}
	}
void Render() {
    _canvas->clear();
    
    GT::Point p1(100, 100, GT::RGBA(255, 0, 0));
    GT::Point p2(200, 100, GT::RGBA(255, 0, 0));
    GT::Point p3(150, 150, GT::RGBA(255, 0, 0));

    _canvas->drawTriangeFlat(p1, p2, p3);

    //在这里画到设备上,hMem相当于缓冲区
    BitBlt(hDC, 0, 0, wWidth, wHeight, hMem, 0, 0, SRCCOPY);
}

是白色的,原因是上节的color插值没有变化,不过原理是对的。

实现:任意(非平顶/平底)三角形绘制

先给三个点按y值排序,然后分上下两个平顶/平底三角形

	void Canvas::drawTriange(Point p1, Point p2, Point p3) {
		std::vector<Point>pVec;
		pVec.push_back(p1);
		pVec.push_back(p2);
		pVec.push_back(p3);

		std::sort(pVec.begin(), pVec.end(), [](const Point& p1, const Point& p2) {
			return p1.m_y > p2.m_y;
			});

		//获取三个排序好的点
		Point ptMax = pVec[0];
		Point ptMid = pVec[1];
		Point ptMin = pVec[2];

		//判断是否为平顶平底三角形
		if (ptMax.m_y == ptMid.m_y) {
			drawTriangeFlat(ptMax, ptMid, ptMin);
			return;
		}
		if (ptMin.m_y == ptMid.m_y) {
			drawTriangeFlat(ptMin, ptMid, ptMax);
			return;
		}

		//计算直线参数k b
		float k = 0.0;
		if (ptMax.m_x != ptMin.m_x) {
			k = (float)(ptMax.m_y - ptMin.m_y) / (float)(ptMax.m_x - ptMin.m_x);
		}
		float b = (float)ptMax.m_y - (float)ptMax.m_x * k;
		Point newPoint(0, 0, RGBA(255, 0, 0));
		newPoint.m_y = ptMid.m_y;
		if (k == 0) {
			newPoint.m_x = ptMax.m_x;
		}
		else {
			newPoint.m_x = ((float)newPoint.m_y - b) / k;
		}

		//分别绘制这两个平顶/平底三角形
		drawTriangeFlat(ptMid, newPoint, ptMax);
		drawTriangeFlat(ptMid, newPoint, ptMin);

		return;
	}
void Render() {
    _canvas->clear();
    
    GT::Point p1(700, 500, GT::RGBA(255, 0, 0));
    GT::Point p2(200, 100, GT::RGBA(255, 0, 0));
    GT::Point p3(150, 150, GT::RGBA(255, 0, 0));

    _canvas->drawTriange(p1, p2, p3);

    //在这里画到设备上,hMem相当于缓冲区
    BitBlt(hDC, 0, 0, wWidth, wHeight, hMem, 0, 0, SRCCOPY);
}

优化2:若三角形是否在屏幕范围内or屏幕是否在一个巨大的三角形内

此时需要判断,避免无意义的遍历

判断:

  • 三角形的点是否在矩形内
  • 矩形的点是否在三角形内

实现如下:

	bool Canvas::judgeInTriangle(Point pt, std::vector<Point> _ptArray){
		Point pt1 = _ptArray[0];
		Point pt2 = _ptArray[1];
		Point pt3 = _ptArray[2];

		int x = pt.m_x;
		int y = pt.m_y;

		//三个k
		float k1 = (float)(pt1.m_y - pt2.m_y) / (float)(pt1.m_x - pt2.m_x);
		float k2 = (float)(pt1.m_y - pt3.m_y) / (float)(pt1.m_x - pt3.m_x);
		float k3 = (float)(pt3.m_y - pt2.m_y) / (float)(pt3.m_x - pt2.m_x);

		//三个b
		float b1 = (float)pt1.m_y - k1 * pt1.m_x;
		float b2 = (float)pt3.m_y - k2 * pt3.m_x;
		float b3 = (float)pt2.m_y - k3 * pt2.m_x;

		//是否在三角形内
		float judge1 = (y - (k1 * x + b1)) * (pt3.m_y - (k1 * pt3.m_x + b1));
		float judge2 = (y - (k2 * x + b2)) * (pt2.m_y - (k2 * pt2.m_x + b2));
		float judge3 = (y - (k3 * x + b3)) * (pt1.m_y - (k3 * pt1.m_x + b3));

		if (judge1 > 0 && judge2 > 0 && judge3 > 0)
		{
			return true;
		}

		return false;
	}

	void Canvas::drawTriange(Point p1, Point p2, Point p3) {
		std::vector<Point>pVec;
		pVec.push_back(p1);
		pVec.push_back(p2);
		pVec.push_back(p3);

		std::sort(pVec.begin(), pVec.end(), [](const Point& p1, const Point& p2) {
			return p1.m_y > p2.m_y;
			});

		//获取三个排序好的点
		Point ptMax = pVec[0];
		Point ptMid = pVec[1];
		Point ptMin = pVec[2];

		//判断是否为平顶平底三角形
		if (ptMax.m_y == ptMid.m_y) {
			drawTriangeFlat(ptMax, ptMid, ptMin);
			return;
		}
		if (ptMin.m_y == ptMid.m_y) {
			drawTriangeFlat(ptMin, ptMid, ptMax);
			return;
		}

		//计算直线参数k b
		float k = 0.0;
		if (ptMax.m_x != ptMin.m_x) {
			k = (float)(ptMax.m_y - ptMin.m_y) / (float)(ptMax.m_x - ptMin.m_x);
		}
		float b = (float)ptMax.m_y - (float)ptMax.m_x * k;
		Point newPoint(0, 0, RGBA(255, 0, 0));
		newPoint.m_y = ptMid.m_y;
		if (k == 0) {
			newPoint.m_x = ptMax.m_x;
		}
		else {
			newPoint.m_x = ((float)newPoint.m_y - b) / k;
		}

		//分别绘制这两个平顶/平底三角形
		drawTriangeFlat(ptMid, newPoint, ptMax);
		drawTriangeFlat(ptMid, newPoint, ptMin);

		return;
	}

优化3:对优化2中的情况进行剪裁

在Canvas::drawTriangeFlat中进行剪裁:

	void Canvas::drawTriangeFlat(Point pFlat1, Point pFlat2, Point pt){
		//两边直线斜率
		float k1 = 0.0;
		float k2 = 0.0;

		if (pFlat1.m_x != pt.m_x) {
			k1 = (float)(pFlat1.m_y - pt.m_y) / (float)(pFlat1.m_x - pt.m_x);
		}
		if (pFlat2.m_x != pt.m_x) {
			k2 = (float)(pFlat2.m_y - pt.m_y) / (float)(pFlat2.m_x - pt.m_x);
		}

		//两直线b值
		float b1 = (float)pt.m_y - (float)pt.m_x * k1;
		float b2 = (float)pt.m_y - (float)pt.m_x * k2;

		//做垂线
		int yStart = MIN(pt.m_y, pFlat1.m_y);
		int yEnd = MAX(pt.m_y, pFlat1.m_y);
		if (yStart < 0)yStart = 0;
		if (yEnd > m_height)yEnd = m_height - 1;


		for (int y = yStart; y <= yEnd; ++y) {
			int x1 = 0;
			//判断是否为直角三角形
			if (k1 == 0) {
				x1 = pFlat1.m_x;
			}
			else {
				x1 = ((float)y - b1) / k1;
			}
			//剪裁
			if (x1 < 0)x1 = 0;
			if (x1 > m_width)x1 = m_width - 1;

			int x2 = 0;
			//判断是否为直角三角形
			if (k2 == 0) {
				x2 = pFlat2.m_x;
			}
			else {
				x2 = ((float)y - b2) / k2;
			}
			//剪裁
			if (x2 < 0)x2 = 0;
			if (x2 > m_width)x2 = m_width - 1;

			//构建这两个点
			Point pt1(x1, y, RGBA(255, 0, 0));
			Point pt2(x2, y, RGBA(255, 0, 0));

			//绘制直线
			drawLine(pt1, pt2);
		}
	}

测试效果:

 也可以设置为一个巨大的三角形,大于屏幕空间,其速度要由于不做剪裁的方法很多倍。

实现:绘制彩色三角形

根据之前三角形画线的方法,确定两个点,这次是对两个点的RGBA值进行插值过渡。

	void Canvas::drawTriange(Point p1, Point p2, Point p3) {
		std::vector<Point>pVec;
		pVec.push_back(p1);
		pVec.push_back(p2);
		pVec.push_back(p3);

		//是否双方相交
		GT_RECT _rect(0, m_width, 0, m_height);
		while (1) {
			if (judgeInRect(p1, _rect) || 
				judgeInRect(p2, _rect) || 
				(judgeInRect(p3, _rect) ) ) {
				break;
			}
			Point rpt1(0, 0, RGBA());
			Point rpt2(0, m_width, RGBA());
			Point rpt3(0, m_height, RGBA());
			Point rpt4(m_width, m_height, RGBA());
			if (judgeInTriangle(rpt1,pVec) ||
				judgeInTriangle(rpt2,pVec) || 
				judgeInTriangle(rpt3,pVec) || 
				judgeInTriangle(rpt4,pVec)) {
				break;
			}
			return;
		}
		

		std::sort(pVec.begin(), pVec.end(), [](const Point& p1, const Point& p2) {
			return p1.m_y > p2.m_y;
			});

		//获取三个排序好的点
		Point ptMax = pVec[0];
		Point ptMid = pVec[1];
		Point ptMin = pVec[2];

		//判断是否为平顶平底三角形
		if (ptMax.m_y == ptMid.m_y) {
			drawTriangeFlat(ptMax, ptMid, ptMin);
			return;
		}
		if (ptMin.m_y == ptMid.m_y) {
			drawTriangeFlat(ptMin, ptMid, ptMax);
			return;
		}

		//计算直线参数k b
		float k = 0.0;
		if (ptMax.m_x != ptMin.m_x) {
			k = (float)(ptMax.m_y - ptMin.m_y) / (float)(ptMax.m_x - ptMin.m_x);
		}
		float b = (float)ptMax.m_y - (float)ptMax.m_x * k;
		Point newPoint(0, 0, RGBA(255, 0, 0));
		newPoint.m_y = ptMid.m_y;
		if (k == 0) {
			newPoint.m_x = ptMax.m_x;
		}
		else {
			newPoint.m_x = ((float)newPoint.m_y - b) / k;
		}
		float s = (float)(newPoint.m_y - ptMin.m_y) / (float)(ptMax.m_y - ptMin.m_y);
		newPoint.m_color = colorLerp(ptMin.m_color, ptMax.m_color, s);


		//分别绘制这两个平顶/平底三角形
		drawTriangeFlat(ptMid, newPoint, ptMax);
		drawTriangeFlat(ptMid, newPoint, ptMin);

		return;
	}
	void Canvas::drawTriangeFlat(Point pFlat1, Point pFlat2, Point pt){
		//两边直线斜率
		float k1 = 0.0;
		float k2 = 0.0;

		if (pFlat1.m_x != pt.m_x) {
			k1 = (float)(pFlat1.m_y - pt.m_y) / (float)(pFlat1.m_x - pt.m_x);
		}
		if (pFlat2.m_x != pt.m_x) {
			k2 = (float)(pFlat2.m_y - pt.m_y) / (float)(pFlat2.m_x - pt.m_x);
		}

		//两直线b值
		float b1 = (float)pt.m_y - (float)pt.m_x * k1;
		float b2 = (float)pt.m_y - (float)pt.m_x * k2;

		//做垂线
		int yStart = MIN(pt.m_y, pFlat1.m_y);
		int yEnd = MAX(pt.m_y, pFlat1.m_y);
		if (yStart < 0)yStart = 0;
		if (yEnd > m_height)yEnd = m_height - 1;


		//颜色插值相关
		RGBA colorStart1, colorEnd1;
		RGBA colorStart2, colorEnd2;
		if (pt.m_y < pFlat1.m_y) {
			yStart = pt.m_y;
			yEnd = pFlat1.m_y;

			colorStart1 = pt.m_color;
			colorEnd1 = pFlat1.m_color;
			colorStart2 = pt.m_color;
			colorEnd2 = pFlat2.m_color;
		}
		else {
			yStart = pFlat1.m_y;
			yEnd = pt.m_y;

			colorStart1 = pFlat1.m_color;
			colorEnd1 = pt.m_color;
			colorStart2 = pFlat2.m_color;
			colorEnd2 = pt.m_color;
		}
		float yColorStep = 1.0 / ((float)(yEnd - yStart));
		int yColorStart = yStart;


		for (int y = yStart; y <= yEnd; ++y) {
			int x1 = 0;
			//判断是否为直角三角形
			if (k1 == 0) {
				x1 = pFlat1.m_x;
			}
			else {
				x1 = ((float)y - b1) / k1;
			}
			//剪裁
			if (x1 < 0)x1 = 0;
			if (x1 > m_width)x1 = m_width - 1;

			int x2 = 0;
			//判断是否为直角三角形
			if (k2 == 0) {
				x2 = pFlat2.m_x;
			}
			else {
				x2 = ((float)y - b2) / k2;
			}
			//剪裁
			if (x2 < 0)x2 = 0;
			if (x2 > m_width)x2 = m_width - 1;

			//构建这两个点
			float s1 = (float)(y - yColorStart) * yColorStep;
			RGBA _color1 = colorLerp(colorStart1, colorEnd1, s1);
			RGBA _color2 = colorLerp(colorStart2, colorEnd2, s1);
			Point pt1(x1, y, _color1);
			Point pt2(x2, y, _color2);

			//绘制直线
			drawLine(pt1, pt2);
		}
	}
void Render() {
    _canvas->clear();
    
    GT::Point p1(0, 0, GT::RGBA(255, 0, 0));
    GT::Point p2(500, 0, GT::RGBA(0, 255, 0));
    GT::Point p3(250, 200, GT::RGBA(0, 0, 255));

    _canvas->drawTriange(p1, p2, p3);

    //在这里画到设备上,hMem相当于缓冲区
    BitBlt(hDC, 0, 0, wWidth, wHeight, hMem, 0, 0, SRCCOPY);
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值