三維幾何生成:多段線、圓弧

王小于的啦發表於2024-08-21

一、三維空間多段線幾何

1 應用背景

​​  opengl常用glLineWidth命令設定線寬,此線寬在透視投影中不會隨著相機遠近變化而縮放。專案中高版本glLineWidth命令失效,需要考慮如何快速、方便、寬度不變的多段線幾何。方案a:純shader繪製曲線,繪製到一個二維平面上,然後將平面旋轉朝向螢幕,保持平面縮放不變,實現中需要考慮繪製順序,因為失去了深度,沒辦法做深度測試。方案b:傳前一個點和後一個點到頂點屬性中去,使用幾何著色器,將線寬對應的新頂點傳送出去,實現起來還是比較二維,從另外的視角觀察直線會被摺疊。方案c:手動生成每一線段幾何面片,組合所有面片為一個drawable,實現中遇到有線段連線處不光滑、固定線寬等問題,總體上基本和glLineWidth繪製的線效果一致。ps:根據國外部落格學到了方案4,例項化繪製線段技術,實現起來效果很不錯,優法!

2 單個線段幾何生成

​​  如上圖所示,三維線段幾何可以抽象為圓柱體,圓柱體的半徑為線寬,高度為上下底面M和N構成的向量MN長度之差。注意到圓柱體進一步離散化為多面體,我們只需要側邊包圍面,將其展開就得到了長方形。長方形的長為底面周長,高為MN長度。注意到,多段線的線段間即有平移也有旋轉,幾何座標要乘以旋轉矩陣。

void constructSegment(osg::Vec3 firstPt,osg::Vec3 secondPt,float width)
{
	osg::ref_ptr<osg::Vec3Array>  vertices=new osg::Vec3Array;
	osg::ref_ptr<osg::DrawElementsUInt>  sides=new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLE_FAN);
	int numSegments=10;
	float radius=0.5*width;
	auto center=(firstPt+secondPt)*0.5;
	float height=(firstPt-secondPt).length();
	
	float basez=-height*0.5f;
	float topz=height*0.5f;
	
	
	auto dir=firstPt-secondPt;
	osg::Quat rotQuat;
	rotQuat.makeRotate(osg::Z_AXIS,dir);
	
	float angle=0.0f;
	float angleDelta=2.f*M_PI/(float)numSegments;
	
	for(int bodyi=0;bodyi<numSegments;++bodyi,angle+=angleDelta)
	{
		float c=cosf(angle);
		float s=sinf(angle);
		auto P=osg::Vec3(c*radius,s*radius,topz);
		auto Q=osg::Vec3(c*radius,s*radius,topz);
		P=P*osg::Matrix:;rotate(rotQuat)*osg::Matrix::translate(center);
		Q=Q*osg::Matrix:;rotate(rotQuat)*osg::Matrix::translate(center);
		vertices->push_back(P);
		vertices->push_back(Q);		
	}
	//新增圓柱索引
	float curSegmentSize=0;
	for(int i=0;i<numSegments*2-2;i+=2)
	{
		sides->push_back(curSegmentSize+i);
		sides->push_back(curSegmentSize+i+1);
		sides->push_back(curSegmentSize+i+3);
		sides->push_back(curSegmentSize+i+2);
		sides->push_back(curSegmentSize+i);
		sides->push_back(curSegmentSize+i+3);
	}
	sides->push_back(curSegmentSize+numSegments*2-2);
    sides->push_back(curSegmentSize+numSegments*2-1);
    sides->push_back(curSegmentSize+1);
    sides->push_back(curSegmentSize);
    sides->push_back(curSegmentSize+numSegments*2-2);
    sides->push_back(curSegmentSize+1);
}

​​  最佳化:按照圓柱的方式,生成的多段線幾何之間的連線不光滑,且會隨著相機縮放而縮放,因此通常在兩段線之間新增半圓球過渡。

void constructHalfSphere(osg::ref_ptr<osg::Vec3Array>  vertices,osg::ref_ptr<osg::DrawElementsUInt>  sides,osg::Vec3 firstPt,osg::Vec3 secondPt,float radius,int&curSegmentSize,bool isTop)
{
	int numRows=5;
	int numSegments=5;
	float lDelta=M_PI/(float)numRows;
	float vDelta=1.f/(float)numRows;
	float angleDelta=M_PI*2.f/(float)numSegments;
	
	float lBase=-M_PI*0.5f+(isTop?(lDelta*(numRows/2)):0.f);
	float rBase=isTop?(cosf(lBase)*radius):0.f;
	float zBase=isTop?(sinf(lBase)*radius):-radius;
	float vBase=isTop?(vDelta*(numRows/2)):0.f;
	float nzBase=isTop?(sinf(lBase)):-1.f;
	float nRatioBase=isTop?(cosf(lBase)):0.f;
	
	int  rowbegin=isTop:numRows/2:0;
	int  rowend=isTop?numRows:numRows/2;
	
	auto dir=(firstPt-secondPt);
	osg::Quat rotQuat;
	rotQuat.makeRoate(osg::Z_AXIS,dir);
	for(int rowi=rowbegin;rowi<rowend;++rowi)
	{
		float lTop=lBase+lDelta;
		float rTop=cosf(lTop)*radius;
		float zTop=sinf(lTop)*radius;
		float vTop=vBase+vDelta;
		float nzTop=sinf(lTop);
		float nRatioTop=cosf(lTop);
		
		float angle=0.0f;
		for(int topi=0;topi<numSegments;++topi,angle+=angleDelta)
		{
			float c=cosf(angle);
			float s=sinf(angle);
			auto P=osg::Vec3(c*rTop,s*rTop,zTop);
			auto Q=osg::Vec3(c*rBase,s*rBase,zBase);
			P=P*osg::Matrixd::Rotate(rotQuat)*osg::Matrix::translate(isTop?firstPt:secondPt);
			Q=Q*osg::Matrixd::Rotate(rotQuat)*osg::Matrix::translate(isTop?firstPt:secondPt);
			vertices->push_back(P);
			vertices->push_back(Q);
		}
		//新增圓柱索引
		for(int i=0;i<numSegments*2-2;i+=2)
		{
			sides->push_back(curSegmentSize+i);
			sides->push_back(curSegmentSize+i+1);
			sides->push_back(curSegmentSize+i+3);
			sides->push_back(curSegmentSize+i+2);
			sides->push_back(curSegmentSize+i);
			sides->push_back(curSegmentSize+i+3);
		}
		sides->push_back(curSegmentSize+numSegments*2-2);
        sides->push_back(curSegmentSize+numSegments*2-1);
        sides->push_back(curSegmentSize+1);
        sides->push_back(curSegmentSize);
        sides->push_back(curSegmentSize+numSegments*2-2);
        sides->push_back(curSegmentSize+1);
        curSegmentSize+=numSegments*2;
        
        lBase=lTop;
        rBase=rTop;
        zBase=zTop;
        vBase=vTop;
        nzBase=nzTop;
        nRatioBase=nRatioTop;
	}
	return true;
}

3 例項化渲染線條技術

3.1 例項化技術簡介

​​  當你要繪製大量重複的幾何時,如果透過繫結vbo、上傳頂點、呼叫繪製命令的方式會導致幀率急劇下降而卡頓。因為與GPU繪製本身相比,切換上下文,從CPU到GPU慢得多。例項化技術就是使用一個繪製函式,讓GPU利用這些資料繪製多個物體,節省了每次繪製物體時CPU到GPU的通訊。opengl中呼叫命令為:glDrawArraysInstanced和glDrawElementsInstanced。

​​  首先考慮繪製單個線段,對於三維線段,可以傳遞線段起點、終點座標,以及線段頂點為圖元屬性,然後在頂點著色器中將矩形圖元頂點座標變換到合適位置。如下圖所示,已知A、B世界座標,先求得AB的方向向量沿x軸,然後再求沿y軸的垂直向量,依次計算兩個三角形圖元的頂點座標。

std::vector<osg::Vec3> linePoints;
{
	///建立四邊形頂點
	auto vertices=new osg::Vec3Array;
	vertices->push_back(osg::Vec3(0,-0.5,0));
	vertices->push_back(osg::Vec3(0,-0.5,1));
	vertices->push_back(osg::Vec3(0,0.5,1));
	vertices->push_back(osg::Vec3(0,-0.5,0));
	vertices->push_back(osg::Vec3(0,0.5,1));
	vertices->push_back(osg::Vec3(0,0.5,0));
	///建立幾何起點和終點
	auto pointAs=new osg::Vec3Array;
	auto pointBs=new osg::Vec3Array;
	pointAs->push_back(linePoints.front());
	for(int i=1;i<=linePoints.size()-1;i++)
	{
		pointAs->push_back(linePoints[i]);
		pointBs->push_back(linePoints[i]);
	}
	pointBs->push_back(linePoints.back());
	///建立四邊形幾何
	osg::ref_ptr<osg::Geometry> quad=new osg::Geometry;
	quad->setVertexAttribArray(0,vertices,osg::Array::BIND_PER_VERTEX);
	quad->setVertexAttribArray(1,pointAs,osg::Array::BIND_PER_VERTEX);
	quad->setVertexAttribArray(2,pointBs,osg::Array::BIND_PER_VERTEX);
	quad->addPrimitiveSet(new osg::DrawArrays(GL_TRIANGLES,0,6,pointAs->size()));
	quad->setusedisplayList(false);
	quad->setUseVertexBufferObjects(true);
	quad->getOrCreateStateSet()->setAttribute(new osg::VertexAttribDivisor(1,1));
	quad->getOrCreateStateSet()->setAttribute(new osg::VertexAttribDivisor(2,1));
	
	osg::ref_ptr<osg::Geode> geode=new osg::Geode;
	geode->addDrawable(quad);
	
	const char* vertCode=R"(
		#version 330 core
		layout(location=0) in vec3 aPos;
		layout(location=1) in vec3 aPointA;
		layout(location=2) in vec3 aPointB;
		uniform mat4 osg_ModelViewProjectionMatrix;
		uniform mat4 osg_ModelViewMatrix;
		uniform mat4 osg_NormalMatrix;
		uniform vec2 resolution;
		uniform float width=5.0;
		void main(){
			vec4 clip0=osg_ModelViewProjectionMatrix*vec4(aPointA,1.0);
			vec4 clip1=osg_ModelViewProjectionMatrix*vec4(aPointB,1.0);
			vec2 screen0=resolution*(0.5*clip0.xy/clip0.w+0.5);
			vec2 screen1=resolution*(0.5*clip1.xy/clip1.w+0.5);
			vec2 xBasis=normalize(screen1.xy-screen0.xy);
			float length=length(screen1.xy-screen0.xy);
			vec2 yBasis=vec2(-xBasis.y,xBasis.x);
			vec2 pt=screen0+lenthAB*xBasis*aPos.z+width*yBasis*aPos.y;
			gl_Position=vec4(clip0.w*(2.0*pt/resolution-1.0),clip0.z,clip0.w);
		}	
	)";
	const char* fragCode=R"(
		uniform vec3 front;
		uniform float opacity;
		out vec4 fragColor;
		void main()
		{
			fragColor=vec4(front,opacity);
		}
	)";

	osg::ref_ptr<osg::Shader> vertShader=new osg::Shader(osg::Shader::VERTEX,vertCode);
    osg::ref_ptr<osg::Shader> fragShader=new osg::Shader(osg::Shader::FRAGMENT,fragCode);
    osg::ref_ptr<osg::Program>  program=new osg::Program;
    program->addShader(vertShader);
    program->addShader(fragShader);
    geode->getOrCreateStateSet()->setAttributeAndModes(program,OVERRIDE_ON);
}

二、三維空間圓弧面幾何

1 應用背景

​​  專案中有需求繪製三維的圓弧面幾何。可以按照扇形的方式生成。

2 圓弧面幾何

void constructSector(osg::Vec3 center,osg::Vec3 left,osg::Vec3 right,float radius,osg::Vec3 color,float radius,float width)
{
    ///建立扇形頂點
	osg::ref_ptr<osg::Vec3Array>  vertices=new osg::Vec3Array;
	///建立扇形法線
    osg::ref_ptr<osg::Vec3Array>  normals=new osg::Vec3Array;
    ///建立扇形拓撲
	osg::ref_ptr<osg::DrawElementsUInt>  sides=new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLE_FAN);
	int index=0;
	///使用球面插值計算圓弧上座標
	auto LC=left-center;
	auto RC=right-center;
	LC.normalize();
	RC.normalize();
	osg::Quat newQuat,startQuat,endQuat;
	startQuat.makeRotate(LC,LC);
	endQuat.makeRotate(LC,RC);
	vertices->push_back(center);
	sides->push_back(index++);	
	///生成正面
	for(float i=0;i<=1;i+=0.02)
	{
		newQuat.slerp(i,startQuat,endQuat);
		auto point=center+LC*osg::Matrix(newQuat)*radius;
		vertices->push_back(point);
		normals->push_back(osg::Vec3(0,0,1));
		sides->push_back(index++);	
	}
	vertices->push_back(center);
	sides->push_back(index++);	
	///生成反面
	for(float i=1;i》=0;i-=0.02)
	{
		newQuat.slerp(i,startQuat,endQuat);
		auto point=center+LC*osg::Matrix(newQuat)*radius;
		vertices->push_back(point);
		normals->push_back(osg::Vec3(0,0,-1));
		sides->push_back(index++);	
	}
	///建立扇形幾何
	 osg::ref_ptr<osg::Geometry>  sector=new osg::Geometry;
	 sector->setVertexArray(vertices);
	 sector->setNormalArray(normals);
	 sector->addPrimitiveSet(sides);
}

相關文章