// This file is part of OpenCV project. // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html #include "precomp.hpp" namespace cv { TriangleRasterizeSettings::TriangleRasterizeSettings() { shadingType = RASTERIZE_SHADING_SHADED; cullingMode = RASTERIZE_CULLING_CW; glCompatibleMode = RASTERIZE_COMPAT_DISABLED; } static void drawTriangle(Vec4f verts[3], Vec3f colors[3], Mat& depthBuf, Mat& colorBuf, TriangleRasterizeSettings settings) { // this will be useful during refactoring // if there's gonna be more supported data types CV_DbgAssert(depthBuf.empty() || depthBuf.type() == CV_32FC1); CV_DbgAssert(colorBuf.empty() || colorBuf.type() == CV_32FC3); // any of buffers can be empty int width = std::max(colorBuf.cols, depthBuf.cols); int height = std::max(colorBuf.rows, depthBuf.rows); Point minPt(width, height), maxPt(0, 0); for (int i = 0; i < 3; i++) { // round down to cover the whole pixel int x = (int)(verts[i][0]), y = (int)(verts[i][1]); minPt.x = std::min( x, minPt.x); minPt.y = std::min( y, minPt.y); maxPt.x = std::max(x + 1, maxPt.x); maxPt.y = std::max(y + 1, maxPt.y); } minPt.x = std::max(minPt.x, 0); maxPt.x = std::min(maxPt.x, width); minPt.y = std::max(minPt.y, 0); maxPt.y = std::min(maxPt.y, height); Point2f a(verts[0][0], verts[0][1]), b(verts[1][0], verts[1][1]), c(verts[2][0], verts[2][1]); Point2f bc = b - c, ac = a - c; float d = ac.x*bc.y - ac.y*bc.x; // culling and degenerated triangle removal if ((settings.cullingMode == RASTERIZE_CULLING_CW && d <= 0) || (settings.cullingMode == RASTERIZE_CULLING_CCW && d >= 0) || (abs(d) < 1e-6)) { return; } float invd = 1.f / d; Vec3f zinv { verts[0][2], verts[1][2], verts[2][2] }; Vec3f w { verts[0][3], verts[1][3], verts[2][3] }; for (int y = minPt.y; y < maxPt.y; y++) { for (int x = minPt.x; x < maxPt.x; x++) { Point2f p(x + 0.5f, y + 0.5f), pc = p - c; // barycentric coordinates Vec3f f; f[0] = ( pc.x * bc.y - pc.y * bc.x) * invd; f[1] = ( pc.y * ac.x - pc.x * ac.y) * invd; f[2] = 1.f - f[0] - f[1]; // if inside the triangle if ((f[0] >= 0) && (f[1] >= 0) && (f[2] >= 0)) { bool update = false; if (!depthBuf.empty()) { float zCurrent = depthBuf.at(height - 1 - y, x); float zNew = f[0] * zinv[0] + f[1] * zinv[1] + f[2] * zinv[2]; if (zNew < zCurrent) { update = true; depthBuf.at(height - 1 - y, x) = zNew; } } else // RASTERIZE_SHADING_WHITE { update = true; } if (!colorBuf.empty() && update) { Vec3f color; if (settings.shadingType == RASTERIZE_SHADING_WHITE) { color = { 1.f, 1.f, 1.f }; } else if (settings.shadingType == RASTERIZE_SHADING_FLAT) { color = colors[0]; } else // TriangleShadingType::Shaded { float zInter = 1.0f / (f[0] * w[0] + f[1] * w[1] + f[2] * w[2]); color = { 0, 0, 0 }; for (int j = 0; j < 3; j++) { color += (f[j] * w[j]) * colors[j]; } color *= zInter; } colorBuf.at(height - 1 - y, x) = color; } } } } } // values outside of [zNear, zFar] have to be restored // [0, 1] -> [zNear, zFar] static void linearizeDepth(const Mat& inbuf, const Mat& validMask, Mat outbuf, double zFar, double zNear) { CV_Assert(inbuf.type() == CV_32FC1); CV_Assert(validMask.type() == CV_8UC1 || validMask.type() == CV_8SC1 || validMask.type() == CV_BoolC1); CV_Assert(outbuf.type() == CV_32FC1); CV_Assert(outbuf.size() == inbuf.size()); float scaleNear = (float)(1.0 / zNear); float scaleFar = (float)(1.0 / zFar); for (int y = 0; y < inbuf.rows; y++) { const float* inp = inbuf.ptr(y); const uchar * validPtr = validMask.ptr(y); float * outp = outbuf.ptr(y); for (int x = 0; x < inbuf.cols; x++) { if (validPtr[x]) { float d = inp[x]; // precision-optimized version of this: //float z = - zFar * zNear / (d * (zFar - zNear) - zFar); float z = 1.f / ((1.f - d) * scaleNear + d * scaleFar ); outp[x] = z; } } } } // [zNear, zFar] -> [0, 1] static void invertDepth(const Mat& inbuf, Mat& outbuf, Mat& validMask, double zNear, double zFar) { CV_Assert(inbuf.type() == CV_32FC1); outbuf.create(inbuf.size(), CV_32FC1); validMask.create(inbuf.size(), CV_8UC1); float fNear = (float)zNear, fFar = (float)zFar; float zadd = (float)(zFar / (zFar - zNear)); float zmul = (float)(-zNear * zFar / (zFar - zNear)); for (int y = 0; y < inbuf.rows; y++) { const float * inp = inbuf.ptr(y); float * outp = outbuf.ptr(y); uchar * validPtr = validMask.ptr(y); for (int x = 0; x < inbuf.cols; x++) { float z = inp[x]; uchar m = (z >= fNear) && (z <= fFar); z = std::max(std::min(z, fFar), fNear); // precision-optimized version of this: // outp[x] = (z - zNear) / z * zFar / (zFar - zNear); outp[x] = zadd + zmul / z; validPtr[x] = m; } } } static void triangleRasterizeInternal(InputArray _vertices, InputArray _indices, InputArray _colors, Mat& colorBuf, Mat& depthBuf, InputArray world2cam, double fovyRadians, double zNear, double zFar, const TriangleRasterizeSettings& settings) { CV_Assert(world2cam.type() == CV_32FC1 || world2cam.type() == CV_64FC1); CV_Assert((world2cam.size() == Size {4, 3}) || (world2cam.size() == Size {4, 4})); CV_Assert((fovyRadians > 0) && (fovyRadians < CV_PI)); CV_Assert(zNear > 0); CV_Assert(zFar > zNear); Mat cpMat; world2cam.getMat().convertTo(cpMat, CV_64FC1); Matx44d camPoseMat = Matx44d::eye(); for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { camPoseMat(i, j) = cpMat.at(i, j); } } if(_indices.empty()) { return; } CV_CheckFalse(_vertices.empty(), "No vertices provided along with indices array"); Mat vertices, colors, triangles; int nVerts = 0, nColors = 0, nTriangles = 0; int vertexType = _vertices.type(); CV_Assert(vertexType == CV_32FC1 || vertexType == CV_32FC3); vertices = _vertices.getMat(); // transform 3xN matrix to Nx3, except 3x3 if ((_vertices.channels() == 1) && (_vertices.rows() == 3) && (_vertices.cols() != 3)) { vertices = vertices.t(); } // This transposition is performed on 1xN matrix so it's almost free in terms of performance vertices = vertices.reshape(3, 1).t(); nVerts = (int)vertices.total(); int indexType = _indices.type(); CV_Assert(indexType == CV_32SC1 || indexType == CV_32SC3); triangles = _indices.getMat(); // transform 3xN matrix to Nx3, except 3x3 if ((_indices.channels() == 1) && (_indices.rows() == 3) && (_indices.cols() != 3)) { triangles = triangles.t(); } // This transposition is performed on 1xN matrix so it's almost free in terms of performance triangles = triangles.reshape(3, 1).t(); nTriangles = (int)triangles.total(); if (!_colors.empty()) { int colorType = _colors.type(); CV_Assert(colorType == CV_32FC1 || colorType == CV_32FC3); colors = _colors.getMat(); // transform 3xN matrix to Nx3, except 3x3 if ((_colors.channels() == 1) && (_colors.rows() == 3) && (_colors.cols() != 3)) { colors = colors.t(); } colors = colors.reshape(3, 1).t(); nColors = (int)colors.total(); CV_Assert(nColors == nVerts); } // any of buffers can be empty Size imgSize {std::max(colorBuf.cols, depthBuf.cols), std::max(colorBuf.rows, depthBuf.rows)}; // world-to-camera coord system Matx44d lookAtMatrix = camPoseMat; double ys = 1.0 / std::tan(fovyRadians / 2); double xs = ys / (double)imgSize.width * (double)imgSize.height; double zz = (zNear + zFar) / (zNear - zFar); double zw = 2.0 * zFar * zNear / (zNear - zFar); // camera to NDC: [-1, 1]^3 Matx44d perspectMatrix (xs, 0, 0, 0, 0, ys, 0, 0, 0, 0, zz, zw, 0, 0, -1, 0); Matx44f mvpMatrix = perspectMatrix * lookAtMatrix; // vertex transform stage Mat screenVertices(vertices.size(), CV_32FC4); for (int i = 0; i < nVerts; i++) { Vec3f vglobal = vertices.at(i); float x_num = std::fma(mvpMatrix(0,0), vglobal[0], std::fma(mvpMatrix(0,1), vglobal[1], std::fma(mvpMatrix(0,2), vglobal[2], mvpMatrix(0,3)))); float y_num = std::fma(mvpMatrix(1,0), vglobal[0], std::fma(mvpMatrix(1,1), vglobal[1], std::fma(mvpMatrix(1,2), vglobal[2], mvpMatrix(1,3)))); float z_num = std::fma(mvpMatrix(2,0), vglobal[0], std::fma(mvpMatrix(2,1), vglobal[1], std::fma(mvpMatrix(2,2), vglobal[2], mvpMatrix(2,3)))); float w_num = std::fma(mvpMatrix(3,0), vglobal[0], std::fma(mvpMatrix(3,1), vglobal[1], std::fma(mvpMatrix(3,2), vglobal[2], mvpMatrix(3,3)))); float invw = 1.f / w_num; // [-1, 1]^3 => [0, width] x [0, height] x [0, 1] Vec4f vscreen = { std::fma(x_num * invw, 0.5f * (float)imgSize.width, 0.5f * (float)imgSize.width), std::fma(y_num * invw, 0.5f * (float)imgSize.height, 0.5f * (float)imgSize.height), std::fma(z_num * invw, 0.5f, 0.5f), invw }; screenVertices.at(i) = vscreen; } // draw stage for (int t = 0; t < nTriangles; t++) { Vec3i tri = triangles.at(t); Vec3f col[3]; Vec4f ver[3]; for (int i = 0; i < 3; i++) { int idx = tri[i]; CV_DbgAssert(idx >= 0 && idx < nVerts); col[i] = colors.empty() ? Vec3f::all(0) : colors.at(idx); ver[i] = screenVertices.at(idx); } drawTriangle(ver, col, depthBuf, colorBuf, settings); } } void triangleRasterizeDepth(InputArray _vertices, InputArray _indices, InputOutputArray _depthBuf, InputArray world2cam, double fovY, double zNear, double zFar, const TriangleRasterizeSettings& settings) { CV_Assert(!_depthBuf.empty()); CV_Assert(_depthBuf.type() == CV_32FC1); Mat emptyColorBuf; // out-of-range values from user-provided depthBuf should not be altered, let's mark them Mat_ validMask; Mat depthBuf; if (settings.glCompatibleMode == RASTERIZE_COMPAT_INVDEPTH) { depthBuf = _depthBuf.getMat(); } else // RASTERIZE_COMPAT_DISABLED { invertDepth(_depthBuf.getMat(), depthBuf, validMask, zNear, zFar); } triangleRasterizeInternal(_vertices, _indices, noArray(), emptyColorBuf, depthBuf, world2cam, fovY, zNear, zFar, settings); if (settings.glCompatibleMode == RASTERIZE_COMPAT_DISABLED) { linearizeDepth(depthBuf, validMask, _depthBuf.getMat(), zFar, zNear); } } void triangleRasterizeColor(InputArray _vertices, InputArray _indices, InputArray _colors, InputOutputArray _colorBuf, InputArray world2cam, double fovY, double zNear, double zFar, const TriangleRasterizeSettings& settings) { CV_Assert(!_colorBuf.empty()); CV_Assert(_colorBuf.type() == CV_32FC3); Mat colorBuf = _colorBuf.getMat(); Mat depthBuf; if (_colors.empty()) { // full white shading does not require depth test CV_Assert(settings.shadingType == RASTERIZE_SHADING_WHITE); } else { // internal depth buffer is not exposed outside depthBuf.create(_colorBuf.size(), CV_32FC1); depthBuf.setTo(1.0); } triangleRasterizeInternal(_vertices, _indices, _colors, colorBuf, depthBuf, world2cam, fovY, zNear, zFar, settings); } void triangleRasterize(InputArray _vertices, InputArray _indices, InputArray _colors, InputOutputArray _colorBuffer, InputOutputArray _depthBuffer, InputArray world2cam, double fovyRadians, double zNear, double zFar, const TriangleRasterizeSettings& settings) { if (_colors.empty()) { CV_Assert(settings.shadingType == RASTERIZE_SHADING_WHITE); } CV_Assert(!_colorBuffer.empty()); CV_Assert(_colorBuffer.type() == CV_32FC3); CV_Assert(!_depthBuffer.empty()); CV_Assert(_depthBuffer.type() == CV_32FC1); CV_Assert(_depthBuffer.size() == _colorBuffer.size()); Mat colorBuf = _colorBuffer.getMat(); // out-of-range values from user-provided depthBuf should not be altered, let's mark them Mat_ validMask; Mat depthBuf; if (settings.glCompatibleMode == RASTERIZE_COMPAT_INVDEPTH) { depthBuf = _depthBuffer.getMat(); } else // RASTERIZE_COMPAT_DISABLED { invertDepth(_depthBuffer.getMat(), depthBuf, validMask, zNear, zFar); } triangleRasterizeInternal(_vertices, _indices, _colors, colorBuf, depthBuf, world2cam, fovyRadians, zNear, zFar, settings); if (settings.glCompatibleMode == RASTERIZE_COMPAT_DISABLED) { linearizeDepth(depthBuf, validMask, _depthBuffer.getMat(), zFar, zNear); } } } // namespace cv