#include "SourceStamp.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { struct FVertex { float Position[3]; float Normal[3]; float Height; float WorldPosition[3]; float TileData[4]; float Material[4]{}; }; struct FScene { std::uint32_t VoxelSizeMicrons=0; std::array CameraMeters{}; double YawDegrees=0.0,PitchDegrees=0.0; float MinimumHeight=std::numeric_limits::infinity(); float MaximumHeight=-std::numeric_limits::infinity(); std::vector Vertices; std::vector Indices; std::vector SurfaceIndices; struct FTileRange { std::array Key{}; std::size_t IndexBegin=0; std::uint32_t IndexCount=0; std::vector FaceMasks; }; std::vector Tiles; std::uint64_t Triangles=0,SkirtTriangles=0; std::uint64_t VisibleSkirtTriangles=0,HiddenSameLodSkirtTriangles=0; std::uint64_t DrawnTriangles=0; std::uint32_t TileCount=0; }; struct FTileKeyHash { std::size_t operator()(const std::array& Key) const noexcept { std::size_t Hash=0xcbf29ce484222325ull; for(const std::int32_t Value:Key) { Hash^=static_cast(Value); Hash*=0x100000001b3ull; } return Hash; } }; std::int32_t FloorDiv(std::int64_t Value,std::int64_t Divisor) { const std::int64_t Quotient=Value/Divisor, Remainder=Value%Divisor; return static_cast(Quotient-((Remainder<0)?1:0)); } std::uint8_t ComputeVisibleSkirtFaceMask( const std::array& Tile, const std::unordered_set,FTileKeyHash>& Desired) { const std::int64_t Extent=static_cast(32)<(Tile[0])*Extent, static_cast(Tile[1])*Extent,static_cast(Tile[2])*Extent}; std::uint8_t Mask=0; for(std::int32_t Axis=0;Axis<3;++Axis)for(std::int32_t Side=0;Side<2;++Side) { std::int64_t Probe[3]={Origin[0]+Extent/2,Origin[1]+Extent/2, Origin[2]+Extent/2}; Probe[Axis]=Side?Origin[Axis]+Extent:Origin[Axis]-1; std::int32_t NeighborLevel=-1; for(std::int32_t Level=0;Level<=8;++Level) { const std::int64_t NeighborExtent=static_cast(32)< Neighbor={ FloorDiv(Probe[0],NeighborExtent),FloorDiv(Probe[1],NeighborExtent), FloorDiv(Probe[2],NeighborExtent),Level}; if(Desired.contains(Neighbor)) {NeighborLevel=Level;break;} } if(NeighborLevel<0||NeighborLevel!=Tile[3]) Mask|=static_cast(1u<<(Axis*2+Side)); } return Mask; } template bool Read(std::ifstream& File,T& Value) { File.read(reinterpret_cast(&Value),sizeof(Value)); return static_cast(File); } bool LoadScene(const std::filesystem::path& Path,FScene& Scene,std::string& Error) { std::ifstream File(Path,std::ios::binary); if(!File){Error="cannot open mesh scene";return false;} char Magic[8]{}; File.read(Magic,sizeof(Magic)); const bool HasMaterials=File&&std::memcmp(Magic,"VFMSH002",8)==0; if(!File||(!HasMaterials&&std::memcmp(Magic,"VFMSH001",8)!=0)) {Error="mesh scene magic is invalid";return false;} std::uint32_t Version=0,ExpectedTiles=0; if(!Read(File,Version)||!Read(File,Scene.VoxelSizeMicrons)||!Read(File,ExpectedTiles) ||Version!=(HasMaterials?2u:1u)||Scene.VoxelSizeMicrons==0||ExpectedTiles>1000000) {Error="mesh scene header is malformed";return false;} for(double& Value:Scene.CameraMeters)if(!Read(File,Value)) {Error="mesh scene camera is truncated";return false;} if(!Read(File,Scene.YawDegrees)||!Read(File,Scene.PitchDegrees)) {Error="mesh scene camera orientation is truncated";return false;} Scene.TileCount=ExpectedTiles; std::uint64_t TotalVertices=0,TotalIndices=0; for(std::uint32_t Tile=0;Tile10000000||IndexCount>60000000||MaskCount>20000000 ||IndexCount%3!=0||(MaskCount!=0&&MaskCount!=IndexCount/3) ||Key[3]<0||Key[3]>8) {Error="tile mesh header is malformed";return false;} if(TotalVertices+VertexCount>std::numeric_limits::max() ||TotalIndices+IndexCount>std::numeric_limits::max()) {Error="mesh scene exceeds supported index space";return false;} const std::size_t Base=Scene.Vertices.size(); Scene.Vertices.resize(Base+VertexCount); Scene.Indices.reserve(Scene.Indices.size()+IndexCount); for(std::size_t I=Base;I(Ids[0]); Scene.Vertices[I].Material[1]=static_cast(Weights[0])/65535.0f; Scene.Vertices[I].Material[2]=static_cast(Ids[1]); Scene.Vertices[I].Material[3]=static_cast(Weights[1])/65535.0f; (void)Host; } const double TileVoxels=static_cast(32u<(TileVoxels *static_cast(Scene.VoxelSizeMicrons)/1000000.0); const float TileOriginX=static_cast(static_cast(Key[0]) *TileExtentMeters); const float TileOriginY=static_cast(static_cast(Key[1]) *TileExtentMeters); for(std::size_t I=Base;I(Key[3]); } const std::size_t IndexOffset=Scene.Indices.size(); Scene.Indices.resize(IndexOffset+IndexCount); if(IndexCount)File.read(reinterpret_cast(Scene.Indices.data()+IndexOffset), static_cast(sizeof(std::uint32_t)*IndexCount)); std::vector Masks(MaskCount); if(MaskCount)File.read(reinterpret_cast(Masks.data()),MaskCount); if(!File){Error="tile index or skirt data is truncated";return false;} for(std::size_t Index=IndexOffset;Index=VertexCount) {Error="tile index points outside its vertex array";return false;} Scene.Indices[Index]+=static_cast(Base); } Scene.Triangles+=IndexCount/3; Scene.SkirtTriangles+=SkirtCount; Scene.Tiles.push_back({{Key[0],Key[1],Key[2],Key[3]},IndexOffset, IndexCount,std::move(Masks)}); TotalVertices+=VertexCount;TotalIndices+=IndexCount; } if(File.peek()!=std::char_traits::eof()) {Error="mesh scene has trailing bytes";return false;} if(Scene.Indices.empty()){Error="mesh scene contains no triangles";return false;} std::unordered_set,FTileKeyHash> Desired; Desired.reserve(Scene.Tiles.size()); for(const auto& Tile:Scene.Tiles)Desired.insert(Tile.Key); std::vector VisibleIndices; VisibleIndices.reserve(Scene.Indices.size()); Scene.SurfaceIndices.reserve(Scene.Indices.size()- static_cast(Scene.SkirtTriangles)*3); for(const auto& Tile:Scene.Tiles) { const std::uint8_t VisibleMask=ComputeVisibleSkirtFaceMask(Tile.Key,Desired); for(std::uint32_t Triangle=0;Triangle(Triangle)*3; VisibleIndices.insert(VisibleIndices.end(),Scene.Indices.begin()+Index, Scene.Indices.begin()+Index+3); if(FaceMask==0) Scene.SurfaceIndices.insert(Scene.SurfaceIndices.end(), Scene.Indices.begin()+Index,Scene.Indices.begin()+Index+3); } } Scene.Indices=std::move(VisibleIndices); Scene.DrawnTriangles=Scene.Indices.size()/3; return true; } struct FClipCapResult { std::uint64_t Intersections=0,Loops=0,OpenContours=0,Triangles=0; std::uint64_t DegreeOneNodes=0,BranchNodes=0; std::uint64_t ClosedOpenContours=0; double OpenGapMetres=0.0,NearestOpenGapMetres=0.0; }; FClipCapResult AddClipPlaneCaps(FScene& Scene,const std::array& InputPlane) { FClipCapResult Result; const double NormalLength=std::hypot(InputPlane[0],InputPlane[1],InputPlane[2]); if(!(NormalLength>0.0))return Result; const std::array N={InputPlane[0]/NormalLength, InputPlane[1]/NormalLength,InputPlane[2]/NormalLength}; const double D=InputPlane[3]/NormalLength; const std::array Reference=std::abs(N[2])<0.85 ?std::array{0.0,0.0,1.0}:std::array{0.0,1.0,0.0}; const auto Cross=[](const std::array& A,const std::array& B) { return std::array{A[1]*B[2]-A[2]*B[1], A[2]*B[0]-A[0]*B[2],A[0]*B[1]-A[1]*B[0]}; }; const auto Normalize=[](const std::array& A) { const double Length=std::hypot(A[0],A[1],A[2]); return Length>0.0?std::array{A[0]/Length,A[1]/Length,A[2]/Length} :std::array{1.0,0.0,0.0}; }; const auto Dot=[](const std::array& A,const std::array& B) {return A[0]*B[0]+A[1]*B[1]+A[2]*B[2];}; const std::array U=Normalize(Cross(N,Reference)); const std::array V=Normalize(Cross(N,U)); const std::array Origin={-D*N[0],-D*N[1],-D*N[2]}; using FPointKey=std::pair; std::map NodeForPoint; std::vector> PlanePoints; std::vector> WorldPoints; std::set> Segments; const auto AddPoint=[&](const std::array& Point) { const std::array Relative={Point[0]-Origin[0], Point[1]-Origin[1],Point[2]-Origin[2]}; const std::array PlanePoint={Dot(Relative,U),Dot(Relative,V)}; constexpr double Quantization=1000.0; const FPointKey Key{static_cast(std::llround(PlanePoint[0]*Quantization)), static_cast(std::llround(PlanePoint[1]*Quantization))}; const auto Found=NodeForPoint.find(Key); if(Found!=NodeForPoint.end())return Found->second; const std::size_t Index=PlanePoints.size(); NodeForPoint.emplace(Key,Index);PlanePoints.push_back(PlanePoint); WorldPoints.push_back(Point);return Index; }; for(std::size_t Triangle=0;Triangle+2,3> Points{}; std::array Distances{}; for(std::size_t Vertex=0;Vertex<3;++Vertex) { const auto& Source=Scene.Vertices[Scene.Indices[Triangle+Vertex]]; Points[Vertex]={Source.WorldPosition[0],Source.WorldPosition[1],Source.WorldPosition[2]}; Distances[Vertex]=Dot(N,Points[Vertex])+D; } std::array,3> Hits{};std::size_t HitCount=0; for(std::size_t Edge=0;Edge<3;++Edge) { const std::size_t A=Edge,B=(Edge+1)%3; const double DA=Distances[A],DB=Distances[B]; if(std::abs(DA)<=1.0e-9) { bool Duplicate=false; for(std::size_t I=0;I1.0e-9)||(DA>1.0e-9&&DB< -1.0e-9)) { const double T=DA/(DA-DB); const std::array Hit={ std::lerp(Points[A][0],Points[B][0],T), std::lerp(Points[A][1],Points[B][1],T), std::lerp(Points[A][2],Points[B][2],T)}; bool Duplicate=false; for(std::size_t I=0;I> Adjacency(PlanePoints.size()); for(const auto& [A,B]:Segments) {Adjacency[A].push_back(B);Adjacency[B].push_back(A);} std::vector DegreeOne; for(std::size_t Node=0;Node2)++Result.BranchNodes; } struct FEndpointPair {double Distance=0.0;std::size_t A=0,B=0;}; std::vector EndpointPairs; for(std::size_t I=0;I PairedEndpoints; for(const FEndpointPair& Pair:EndpointPairs) { if(Pair.Distance>1.0)break; if(PairedEndpoints.contains(Pair.A)||PairedEndpoints.contains(Pair.B))continue; const auto ClosingEdge=std::minmax(Pair.A,Pair.B); if(!Segments.emplace(ClosingEdge.first,ClosingEdge.second).second)continue; Adjacency[Pair.A].push_back(Pair.B);Adjacency[Pair.B].push_back(Pair.A); PairedEndpoints.insert(Pair.A);PairedEndpoints.insert(Pair.B); Result.OpenGapMetres=std::max(Result.OpenGapMetres,Pair.Distance); ++Result.ClosedOpenContours; } std::vector> Continuation(Adjacency.size()); for(std::size_t Node=0;Node Pairs; for(std::size_t I=0;I Paired; for(const FTurnPair& Pair:Pairs) { if(Pair.Dot>0.25||Paired.contains(Pair.A)||Paired.contains(Pair.B))continue; Continuation[Node][Pair.A]=Pair.B;Continuation[Node][Pair.B]=Pair.A; Paired.insert(Pair.A);Paired.insert(Pair.B); } } std::set> UsedSegments; std::vector> Loops; for(const auto& Segment:Segments) { if(UsedSegments.contains(Segment))continue; std::vector Loop{Segment.first}; std::size_t Previous=Segment.first,Current=Segment.second; UsedSegments.insert(Segment);bool Closed=false,Branched=false; for(std::size_t Guard=0;Guard<=Segments.size();++Guard) { if(Current==Loop.front()){Closed=true;break;} Loop.push_back(Current); const auto ContinuationIt=Continuation[Current].find(Previous); if(ContinuationIt==Continuation[Current].end()) {Branched=true;break;} const std::size_t Next=ContinuationIt->second; const auto NextEdge=std::minmax(Current,Next); if(UsedSegments.contains(NextEdge)) {Branched=true;break;} UsedSegments.insert(NextEdge);Previous=Current;Current=Next; } if(Closed&&Loop.size()>=3)Loops.push_back(std::move(Loop)); else if(Branched||!Closed)++Result.OpenContours; } Result.Loops=Loops.size(); if(Loops.empty())return Result; struct FPlaneEdge {std::array A{},B{};}; std::vector Edges; std::vector Ys; for(const auto& Loop:Loops) for(std::size_t I=0;I& A, const std::array& B,const std::array& C) { const std::array,3> PlaneVertices={A,B,C}; const std::uint32_t Base=static_cast(Scene.Vertices.size()); for(const auto& PlaneVertex:PlaneVertices) { FVertex Vertex{}; for(std::size_t Axis=0;Axis<3;++Axis) { const double World=Origin[Axis]+U[Axis]*PlaneVertex[0] +V[Axis]*PlaneVertex[1]+N[Axis]*0.001; Vertex.Position[Axis]=static_cast(World); Vertex.WorldPosition[Axis]=static_cast(World); Vertex.Normal[Axis]=static_cast(-N[Axis]); } Vertex.Height=Vertex.Position[2]; Scene.Vertices.push_back(Vertex); } Scene.Indices.insert(Scene.Indices.end(),{Base,Base+1,Base+2}); ++Result.Triangles; }; for(std::size_t Slab=0;Slab+1 Crossings; for(const FPlaneEdge& Edge:Edges) { const double Low=std::min(Edge.A[1],Edge.B[1]); const double High=std::max(Edge.A[1],Edge.B[1]); if(Mid<=Low||Mid>=High)continue; const double T=(Mid-Edge.A[1])/(Edge.B[1]-Edge.A[1]); Crossings.push_back({std::lerp(Edge.A[0],Edge.B[0],T),&Edge}); } std::sort(Crossings.begin(),Crossings.end(),[](const auto& A,const auto& B) {return A.X A={XAt(Left,Y0),Y0}; const std::array B={XAt(Right,Y0),Y0}; const std::array C={XAt(Right,Y1),Y1}; const std::array E={XAt(Left,Y1),Y1}; if(std::abs(A[0]-B[0])+std::abs(E[0]-C[0])<1.0e-7)continue; AppendTriangle(A,B,C);AppendTriangle(A,C,E); } } Scene.DrawnTriangles=Scene.Indices.size()/3; return Result; } EGLDisplay MakeDisplay() { using TGetPlatformDisplay=EGLDisplay(*)(EGLenum,void*,const EGLint*); auto GetPlatformDisplay=reinterpret_cast( eglGetProcAddress("eglGetPlatformDisplayEXT")); #ifdef EGL_PLATFORM_SURFACELESS_MESA if(GetPlatformDisplay) { const EGLDisplay Display=GetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY,nullptr); if(Display!=EGL_NO_DISPLAY)return Display; } #endif return eglGetDisplay(EGL_DEFAULT_DISPLAY); } GLuint CompileShader(GLenum Type,const char* Source) { const GLuint Shader=glCreateShader(Type); glShaderSource(Shader,1,&Source,nullptr); glCompileShader(Shader); GLint Success=GL_FALSE; glGetShaderiv(Shader,GL_COMPILE_STATUS,&Success); if(Success==GL_FALSE) { GLint Length=0;glGetShaderiv(Shader,GL_INFO_LOG_LENGTH,&Length); std::string Log(static_cast(std::max(Length,1)),0); glGetShaderInfoLog(Shader,Length,nullptr,Log.data()); std::fprintf(stderr,"[OffscreenGL] shader compile failed: %s\n",Log.c_str()); glDeleteShader(Shader);return 0; } return Shader; } GLuint MakeProgram() { static constexpr char VertexSource[]=R"GLSL(#version 300 es precision highp float; layout(location=0) in vec3 inCameraPosition; layout(location=1) in vec3 inNormal; layout(location=2) in float inHeight; layout(location=3) in vec3 inWorldPosition; layout(location=4) in vec4 inTileData; layout(location=5) in vec4 inMaterial; out vec3 vNormal; out float vHeight; out vec3 vWorldPosition; out vec4 vTileData; out vec4 vMaterial; uniform float uTanHalfHorizontal; uniform float uTanHalfVertical; uniform float uNear; uniform float uFar; void main() { float z=max(inCameraPosition.z,0.0001); float clipZ=((uFar+uNear)/(uFar-uNear))*z-(2.0*uFar*uNear)/(uFar-uNear); gl_Position=vec4(inCameraPosition.x/uTanHalfHorizontal, inCameraPosition.y/uTanHalfVertical,clipZ,z); vNormal=normalize(inNormal); vHeight=inHeight; vWorldPosition=inWorldPosition; vTileData=inTileData; vMaterial=inMaterial; })GLSL"; static constexpr char FragmentSource[]=R"GLSL(#version 300 es precision highp float; in vec3 vNormal; in float vHeight; in vec3 vWorldPosition; in vec4 vTileData; in vec4 vMaterial; out vec4 outColor; uniform int uReadableMode; uniform int uTileEdges; uniform int uHeightTint; uniform float uHeightCenter; uniform float uHeightRange; uniform float uTileEdgeWidth; uniform vec3 uLightDirection; uniform int uHeadlamp; uniform int uGrazingLight; uniform int uClipEnabled; uniform vec4 uClipPlane; void main() { if(uClipEnabled!=0&&dot(vec4(vWorldPosition,1.0),uClipPlane)<0.0)discard; vec3 n=normalize(vNormal); vec3 color; float illumination=1.0; if(uReadableMode==0) { vec3 normalColor=abs(n)*0.78+vec3(0.12); float band=mod(floor((vHeight+10000.0)/80.0),2.0); vec3 bandColor=mix(vec3(0.24,0.31,0.37),vec3(0.39,0.42,0.35),band); color=mix(bandColor,normalColor,0.68); } else { float heightT=clamp(0.5+(vHeight-uHeightCenter)/max(uHeightRange,0.001),0.0,1.0); float smoothHeightT=heightT*heightT*(3.0-2.0*heightT); vec3 heightTint=uHeightTint!=0 ?mix(vec3(0.62,0.78,1.0),vec3(1.0,0.82,0.62),smoothHeightT) :vec3(1.0); float diffuse=max(dot(n,normalize(uLightDirection)),0.0); illumination=uGrazingLight!=0?0.32+1.20*diffuse: uHeadlamp!=0?0.78+0.34*diffuse:0.62+0.38*diffuse; color=vec3(0.64,0.62,0.56)*heightTint*illumination; } if(uReadableMode!=0) { vec3 resourceColor=vec3(0.0); float resourceWeight=0.0; if(vMaterial.x>0.5) { float w=clamp(vMaterial.y,0.0,1.0); vec3 oreColor=vMaterial.x<1.5?vec3(0.94,0.36,0.12): vMaterial.x<2.5?vec3(0.20,0.48,0.92):vec3(0.25,0.88,0.84); resourceColor+=oreColor*w;resourceWeight+=w; } if(vMaterial.z>0.5) { float w=clamp(vMaterial.w,0.0,1.0); vec3 oreColor=vMaterial.z<1.5?vec3(0.94,0.36,0.12): vMaterial.z<2.5?vec3(0.20,0.48,0.92):vec3(0.25,0.88,0.84); resourceColor+=oreColor*w;resourceWeight+=w; } if(resourceWeight>0.0) color=mix(color,resourceColor/max(resourceWeight,0.001), clamp(resourceWeight,0.0,0.95))*illumination; } if(uTileEdges!=0&&vTileData.z>0.0) { vec2 local=mod(vWorldPosition.xy-vTileData.xy,vTileData.z); vec2 edge=min(local,vec2(vTileData.z)-local); float distanceToTileEdge=min(edge.x,edge.y); float antialias=max(fwidth(distanceToTileEdge),0.005); float line=1.0-smoothstep(uTileEdgeWidth-antialias, uTileEdgeWidth+antialias,distanceToTileEdge); vec3 edgeColor=mix(vec3(0.16,0.22,0.25),vec3(0.30,0.23,0.14), clamp(vTileData.w/8.0,0.0,1.0)); color=mix(color,edgeColor,0.32*line); } outColor=vec4(color,1.0); })GLSL"; const GLuint VS=CompileShader(GL_VERTEX_SHADER,VertexSource); const GLuint FS=CompileShader(GL_FRAGMENT_SHADER,FragmentSource); if(!VS||!FS)return 0; const GLuint Program=glCreateProgram(); glAttachShader(Program,VS);glAttachShader(Program,FS);glLinkProgram(Program); glDeleteShader(VS);glDeleteShader(FS); GLint Success=GL_FALSE;glGetProgramiv(Program,GL_LINK_STATUS,&Success); if(Success==GL_FALSE) { GLint Length=0;glGetProgramiv(Program,GL_INFO_LOG_LENGTH,&Length); std::string Log(static_cast(std::max(Length,1)),0); glGetProgramInfoLog(Program,Length,nullptr,Log.data()); std::fprintf(stderr,"[OffscreenGL] program link failed: %s\n",Log.c_str()); glDeleteProgram(Program);return 0; } return Program; } bool WritePng(const std::filesystem::path& Path,int Width,int Height, const std::vector& Pixels) { std::error_code Error; if(!Path.parent_path().empty())std::filesystem::create_directories(Path.parent_path(),Error); if(Error)return false; FILE* File=std::fopen(Path.c_str(),"wb"); if(!File)return false; png_structp Png=png_create_write_struct(PNG_LIBPNG_VER_STRING,nullptr,nullptr,nullptr); png_infop Info=Png?png_create_info_struct(Png):nullptr; if(!Png||!Info){if(Png)png_destroy_write_struct(&Png,nullptr);std::fclose(File);return false;} if(setjmp(png_jmpbuf(Png))) {png_destroy_write_struct(&Png,&Info);std::fclose(File);return false;} png_init_io(Png,File); png_set_IHDR(Png,Info,static_cast(Width),static_cast(Height), 8,PNG_COLOR_TYPE_RGBA,PNG_INTERLACE_NONE,PNG_COMPRESSION_TYPE_DEFAULT,PNG_FILTER_TYPE_DEFAULT); png_write_info(Png,Info); const std::size_t Stride=static_cast(Width)*4; for(int Y=Height-1;Y>=0;--Y) png_write_row(Png,const_cast(Pixels.data()+static_cast(Y)*Stride)); png_write_end(Png,nullptr); png_destroy_write_struct(&Png,&Info); const bool Good=std::fclose(File)==0; return Good; } bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height, double HorizontalFovDegrees,double HeightRangeMeters, double HeightCenterMeters,bool ReadableMode, bool HeightTint,bool Headlamp,bool TileEdges, double TileEdgeWidthMeters,bool HasLightDirection, const std::array& LightDirection,bool ClipPlaneEnabled, const std::array& ClipPlane) { EGLDisplay Display=MakeDisplay(); EGLint Major=0,Minor=0; if(Display==EGL_NO_DISPLAY||eglInitialize(Display,&Major,&Minor)!=EGL_TRUE) {std::fprintf(stderr,"[OffscreenEGL] eglInitialize failed (0x%x)\n",eglGetError());return false;} if(eglBindAPI(EGL_OPENGL_ES_API)!=EGL_TRUE) {std::fprintf(stderr,"[OffscreenEGL] eglBindAPI failed (0x%x)\n",eglGetError());eglTerminate(Display);return false;} const EGLint ConfigAttributes[]={EGL_SURFACE_TYPE,EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE,EGL_OPENGL_ES3_BIT,EGL_RED_SIZE,8,EGL_GREEN_SIZE,8, EGL_BLUE_SIZE,8,EGL_ALPHA_SIZE,8,EGL_DEPTH_SIZE,24,EGL_NONE}; EGLConfig Config{};EGLint ConfigCount=0; if(eglChooseConfig(Display,ConfigAttributes,&Config,1,&ConfigCount)!=EGL_TRUE||ConfigCount==0) {std::fprintf(stderr,"[OffscreenEGL] no GLES3 pbuffer config (0x%x)\n",eglGetError());eglTerminate(Display);return false;} const EGLint SurfaceAttributes[]={EGL_WIDTH,Width,EGL_HEIGHT,Height,EGL_NONE}; EGLSurface Surface=eglCreatePbufferSurface(Display,Config,SurfaceAttributes); const EGLint ContextAttributes[]={EGL_CONTEXT_CLIENT_VERSION,3,EGL_NONE}; EGLContext Context=eglCreateContext(Display,Config,EGL_NO_CONTEXT,ContextAttributes); if(Surface==EGL_NO_SURFACE||Context==EGL_NO_CONTEXT ||eglMakeCurrent(Display,Surface,Surface,Context)!=EGL_TRUE) {std::fprintf(stderr,"[OffscreenEGL] context/surface creation failed (0x%x)\n",eglGetError()); if(Context!=EGL_NO_CONTEXT)eglDestroyContext(Display,Context); if(Surface!=EGL_NO_SURFACE)eglDestroySurface(Display,Surface); eglTerminate(Display);return false;} const GLubyte* Renderer=glGetString(GL_RENDERER); const GLubyte* GlVersion=glGetString(GL_VERSION); std::printf("[OffscreenEGL] renderer=%s version=%s pbuffer=%dx%d\n", Renderer?reinterpret_cast(Renderer):"unknown", GlVersion?reinterpret_cast(GlVersion):"unknown",Width,Height); GLuint ColorTexture=0,DepthBuffer=0,Framebuffer=0; glGenTextures(1,&ColorTexture);glBindTexture(GL_TEXTURE_2D,ColorTexture); glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,Width,Height,0,GL_RGBA,GL_UNSIGNED_BYTE,nullptr); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); glGenRenderbuffers(1,&DepthBuffer);glBindRenderbuffer(GL_RENDERBUFFER,DepthBuffer); glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH_COMPONENT24,Width,Height); glGenFramebuffers(1,&Framebuffer);glBindFramebuffer(GL_FRAMEBUFFER,Framebuffer); glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,ColorTexture,0); glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_RENDERBUFFER,DepthBuffer); if(glCheckFramebufferStatus(GL_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE) {std::fprintf(stderr,"[OffscreenGL] framebuffer is incomplete\n");return false;} constexpr double Pi=3.14159265358979323846; const double Yaw=Scene.YawDegrees*Pi/180.0,Pitch=Scene.PitchDegrees*Pi/180.0; const std::array Forward={std::sin(Yaw)*std::cos(Pitch), std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)}; const std::array Right={std::cos(Yaw),-std::sin(Yaw),0.0}; const std::array Up={Right[1]*Forward[2]-Right[2]*Forward[1], Right[2]*Forward[0]-Right[0]*Forward[2],Right[0]*Forward[1]-Right[1]*Forward[0]}; const double TanHalfHorizontal=std::tan(HorizontalFovDegrees*Pi/360.0); const double Aspect=static_cast(Width)/Height; const double TanHalfVertical=TanHalfHorizontal/Aspect; const double Near=0.1,Far=20000.0; for(FVertex& Vertex:Scene.Vertices) { const double DX=Vertex.Position[0]-Scene.CameraMeters[0]; const double DY=Vertex.Position[1]-Scene.CameraMeters[1]; const double DZ=Vertex.Position[2]-Scene.CameraMeters[2]; const float CameraX=static_cast(DX*Right[0]+DY*Right[1]+DZ*Right[2]); const float CameraY=static_cast(DX*Up[0]+DY*Up[1]+DZ*Up[2]); const float CameraZ=static_cast(DX*Forward[0]+DY*Forward[1]+DZ*Forward[2]); Vertex.Position[0]=CameraX;Vertex.Position[1]=CameraY;Vertex.Position[2]=CameraZ; } const GLuint Program=MakeProgram(); if(!Program)return false; GLuint Vao=0,Vbo=0,Ebo=0; glGenVertexArrays(1,&Vao);glBindVertexArray(Vao); glGenBuffers(1,&Vbo);glBindBuffer(GL_ARRAY_BUFFER,Vbo); glBufferData(GL_ARRAY_BUFFER,static_cast(Scene.Vertices.size()*sizeof(FVertex)), Scene.Vertices.data(),GL_STATIC_DRAW); glGenBuffers(1,&Ebo);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,Ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER,static_cast(Scene.Indices.size()*sizeof(std::uint32_t)), Scene.Indices.data(),GL_STATIC_DRAW); glEnableVertexAttribArray(0);glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(FVertex), reinterpret_cast(offsetof(FVertex,Position))); glEnableVertexAttribArray(1);glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(FVertex), reinterpret_cast(offsetof(FVertex,Normal))); glEnableVertexAttribArray(2);glVertexAttribPointer(2,1,GL_FLOAT,GL_FALSE,sizeof(FVertex), reinterpret_cast(offsetof(FVertex,Height))); glEnableVertexAttribArray(3);glVertexAttribPointer(3,3,GL_FLOAT,GL_FALSE,sizeof(FVertex), reinterpret_cast(offsetof(FVertex,WorldPosition))); glEnableVertexAttribArray(4);glVertexAttribPointer(4,4,GL_FLOAT,GL_FALSE,sizeof(FVertex), reinterpret_cast(offsetof(FVertex,TileData))); glEnableVertexAttribArray(5);glVertexAttribPointer(5,4,GL_FLOAT,GL_FALSE,sizeof(FVertex), reinterpret_cast(offsetof(FVertex,Material))); glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST); // A covered empty ray represents open air. Keep it visibly blue so it cannot // be mistaken for the old near-black result from a scene that lacked tiles. glDepthFunc(GL_LEQUAL);glClearColor(0.30f,0.48f,0.62f,1.0f); glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glUseProgram(Program); glUniform1f(glGetUniformLocation(Program,"uTanHalfHorizontal"),static_cast(TanHalfHorizontal)); glUniform1f(glGetUniformLocation(Program,"uTanHalfVertical"),static_cast(TanHalfVertical)); glUniform1f(glGetUniformLocation(Program,"uNear"),static_cast(Near)); glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast(Far)); glUniform1i(glGetUniformLocation(Program,"uReadableMode"),ReadableMode?1:0); glUniform1i(glGetUniformLocation(Program,"uTileEdges"),TileEdges?1:0); glUniform1i(glGetUniformLocation(Program,"uHeightTint"),HeightTint?1:0); glUniform1f(glGetUniformLocation(Program,"uHeightCenter"), static_cast(HeightCenterMeters)); glUniform1f(glGetUniformLocation(Program,"uHeightRange"), static_cast(HeightRangeMeters)); glUniform1f(glGetUniformLocation(Program,"uTileEdgeWidth"), static_cast(TileEdgeWidthMeters)); glUniform1i(glGetUniformLocation(Program,"uClipEnabled"),ClipPlaneEnabled?1:0); glUniform1i(glGetUniformLocation(Program,"uHeadlamp"),Headlamp?1:0); glUniform1i(glGetUniformLocation(Program,"uGrazingLight"),HasLightDirection?1:0); glUniform4f(glGetUniformLocation(Program,"uClipPlane"), static_cast(ClipPlane[0]),static_cast(ClipPlane[1]), static_cast(ClipPlane[2]),static_cast(ClipPlane[3])); const std::array ActiveLightDirection=HasLightDirection?LightDirection: Headlamp?std::array{-Forward[0],-Forward[1],-Forward[2]}: std::array{-0.42,-0.58,0.70}; glUniform3f(glGetUniformLocation(Program,"uLightDirection"), static_cast(ActiveLightDirection[0]), static_cast(ActiveLightDirection[1]), static_cast(ActiveLightDirection[2])); glDrawElements(GL_TRIANGLES,static_cast(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr); glFinish(); std::vector Pixels(static_cast(Width)*Height*4); glReadPixels(0,0,Width,Height,GL_RGBA,GL_UNSIGNED_BYTE,Pixels.data()); const GLenum GlError=glGetError(); const bool PngGood=GlError==GL_NO_ERROR&&WritePng(Output,Width,Height,Pixels); if(!PngGood)std::fprintf(stderr,"[OffscreenGL] render or PNG write failed (GL=0x%x)\n",GlError); else { std::error_code SizeError; const auto Bytes=std::filesystem::file_size(Output,SizeError); std::printf("[OffscreenCapture] source_triangles=%llu drawn_triangles=%llu tiles=%u " "skirts=%llu visible_skirt_triangles=%llu hidden_same_lod_skirt_triangles=%llu " "shading=%s height_tint=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n", static_cast(Scene.Triangles), static_cast(Scene.DrawnTriangles),Scene.TileCount, static_cast(Scene.SkirtTriangles), static_cast(Scene.VisibleSkirtTriangles), static_cast(Scene.HiddenSameLodSkirtTriangles), ReadableMode?"readable":"normal",HeightTint?"on":"off", TileEdges?"on":"off",TileEdgeWidthMeters, static_cast(SizeError?0:Bytes),Output.string().c_str()); } glDeleteProgram(Program);glDeleteBuffers(1,&Ebo);glDeleteBuffers(1,&Vbo); glDeleteVertexArrays(1,&Vao);glDeleteFramebuffers(1,&Framebuffer); glDeleteRenderbuffers(1,&DepthBuffer);glDeleteTextures(1,&ColorTexture); eglMakeCurrent(Display,EGL_NO_SURFACE,EGL_NO_SURFACE,EGL_NO_CONTEXT); eglDestroyContext(Display,Context);eglDestroySurface(Display,Surface);eglTerminate(Display); return PngGood; } } int main(int argc,char** argv) { VoxelForgePrintSourceStamp(); if(argc==2&&std::string_view(argv[1])=="--print-source-stamp")return 0; if(argc<3) { std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--height-range-m=N] [--height-center-m=N] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--height-tint=on|off] [--headlamp=on|off] [--light-direction=X,Y,Z] [--tile-edges=on|off] [--tile-skirts=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D] [--focus-region=X,Y,Z,R]\n",argv[0]); return 2; } int Width=2560,Height=1440;double Fov=90.0,HeightRangeMeters=64.0; double HeightCenterMeters=0.0;bool HasHeightCenter=false; bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0; bool HasCamera=false;std::array Camera{}; bool HasLightDirection=false;std::array LightDirection{}; bool ReadableMode=true,HeightTint=true,TileEdges=false,TileSkirts=true,Headlamp=false; double TileEdgeWidthMeters=0.15; bool ClipPlaneEnabled=false;std::array ClipPlane{}; bool FocusRegionEnabled=false;std::array FocusCenter{};double FocusRadius=0.0; for(int I=3;I(Arg.size()),Arg.data());return 2;} } FScene Scene;std::string Error; if(!LoadScene(argv[1],Scene,Error)) {std::fprintf(stderr,"[OffscreenCapture] FAIL: %s\n",Error.c_str());return 2;} if(HasCamera)Scene.CameraMeters=Camera; if(HasYaw)Scene.YawDegrees=Yaw; if(HasPitch)Scene.PitchDegrees=Pitch; if(!HasHeightCenter)HeightCenterMeters=Scene.CameraMeters[2]; const std::size_t CapIndexBegin=Scene.Indices.size(); FClipCapResult ClipCap; if(ClipPlaneEnabled) { ClipCap=AddClipPlaneCaps(Scene,ClipPlane); const bool Capped=ClipCap.Loops>0&&ClipCap.Triangles>0; std::printf("[OffscreenClipCap] segments=%llu loops=%llu open_contours=%llu closed_open_contours=%llu degree_one=%llu branch_nodes=%llu open_gap_m=%.3f nearest_open_gap_m=%.3f cap_triangles=%llu status=%s\n", static_cast(ClipCap.Intersections), static_cast(ClipCap.Loops), static_cast(ClipCap.OpenContours), static_cast(ClipCap.ClosedOpenContours), static_cast(ClipCap.DegreeOneNodes), static_cast(ClipCap.BranchNodes), ClipCap.OpenGapMetres,ClipCap.NearestOpenGapMetres, static_cast(ClipCap.Triangles),Capped?"PASS":"FAIL"); if(!Capped)return 3; } if(!TileSkirts) { const std::size_t SurfaceTriangleCount=Scene.SurfaceIndices.size()/3; std::vector SurfaceAndCaps=std::move(Scene.SurfaceIndices); // Caps were appended after the original mesh indices. Keep them while dropping // the LOD seam skirts, which are useful for streamed play but obscure close captures. if(ClipPlaneEnabled&&Scene.Indices.size()>CapIndexBegin) SurfaceAndCaps.insert(SurfaceAndCaps.end(),Scene.Indices.begin()+CapIndexBegin, Scene.Indices.end()); Scene.Indices=std::move(SurfaceAndCaps); Scene.DrawnTriangles=Scene.Indices.size()/3; std::printf("[OffscreenMeshDisplay] tile_skirts=off kept_surface_triangles=%llu " "cap_triangles=%llu status=PASS\n", static_cast(SurfaceTriangleCount), static_cast(Scene.Indices.size()/3- SurfaceTriangleCount)); } if(FocusRegionEnabled) { std::vector FocusedIndices; FocusedIndices.reserve(Scene.Indices.size()); const double RadiusSquared=FocusRadius*FocusRadius; for(std::size_t Triangle=0;Triangle+2 Center{}; for(std::size_t Corner=0;Corner<3;++Corner) { const auto& Vertex=Scene.Vertices[Scene.Indices[Triangle+Corner]]; for(std::size_t Axis=0;Axis<3;++Axis) Center[Axis]+=Vertex.WorldPosition[Axis]/3.0; } const double DX=Center[0]-FocusCenter[0],DY=Center[1]-FocusCenter[1], DZ=Center[2]-FocusCenter[2]; if(DX*DX+DY*DY+DZ*DZ>RadiusSquared)continue; FocusedIndices.insert(FocusedIndices.end(),Scene.Indices.begin()+Triangle, Scene.Indices.begin()+Triangle+3); } if(FocusedIndices.empty()) std::printf("[OffscreenFocusRegion] center_m=(%.3f,%.3f,%.3f) radius_m=%.3f " "triangles=0 status=EMPTY_FALLBACK full_scene_triangles=%llu\n", FocusCenter[0],FocusCenter[1],FocusCenter[2],FocusRadius, static_cast(Scene.DrawnTriangles)); else { Scene.Indices=std::move(FocusedIndices); Scene.DrawnTriangles=Scene.Indices.size()/3; std::printf("[OffscreenFocusRegion] center_m=(%.3f,%.3f,%.3f) radius_m=%.3f " "triangles=%llu status=PASS\n",FocusCenter[0],FocusCenter[1],FocusCenter[2], FocusRadius,static_cast(Scene.DrawnTriangles)); } } std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f height_center_m=%.3f shade=%s height_tint=%s tile_edges=%s vertices=%zu triangles=%llu\n", 1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2], Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight, HeightCenterMeters, ReadableMode?"readable":"normal",HeightTint?"on":"off", TileEdges?"on":"off",Scene.Vertices.size(), static_cast(Scene.Triangles)); return Render(Scene,argv[2],Width,Height,Fov,HeightRangeMeters,HeightCenterMeters,ReadableMode, HeightTint,Headlamp,TileEdges, TileEdgeWidthMeters,HasLightDirection,LightDirection,ClipPlaneEnabled,ClipPlane)?0:3; }