#include "FluidSim/Desktop/EngineRenderer3D.hpp" #include "FluidSim/Desktop/PressurePalette.hpp" #include "FluidSim/Desktop/PipePath.hpp" #include #include #include #include #include #include namespace fluidsim::port::desktop { namespace { constexpr float pi3=3.14159265359f; struct Vertex {float x,y,z,nx,ny,nz;}; struct Mesh { std::vector vertices; std::vector indices; void draw(const std::vector* colors=nullptr) const { if(colors) {glEnableClientState(GL_COLOR_ARRAY);glColorPointer(3,GL_FLOAT,sizeof(Vec3),colors->data());} else glDisableClientState(GL_COLOR_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glEnableClientState(GL_VERTEX_ARRAY);glEnableClientState(GL_NORMAL_ARRAY); glVertexPointer(3,GL_FLOAT,sizeof(Vertex),vertices.data()); glNormalPointer(GL_FLOAT,sizeof(Vertex),reinterpret_cast(vertices.data())+offsetof(Vertex,nx)); glDrawElements(GL_TRIANGLES,static_cast(indices.size()),GL_UNSIGNED_INT,indices.data()); glDisableClientState(GL_COLOR_ARRAY);glDisableClientState(GL_NORMAL_ARRAY);glDisableClientState(GL_VERTEX_ARRAY); } }; Mesh cylinder(bool half=false) { Mesh m;constexpr unsigned sides=24; for(unsigned i=0;i<=sides;++i) { float a=(half?pi3:0)+(half?pi3:2*pi3)*i/sides,c=std::cos(a),s=std::sin(a); m.vertices.push_back({c,0,s,c,0,s});m.vertices.push_back({c,1,s,c,0,s}); if(i(m.vertices.size());float y=float(cap),normal=cap?1.f:-1.f; m.vertices.push_back({0,y,0,0,normal,0}); for(unsigned i=0;i<=sides;++i) {float a=2*pi3*i/sides;m.vertices.push_back({std::cos(a),y,std::sin(a),0,normal,0});} for(unsigned i=0;i normals{{{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}}; const std::array,6> faces{{ {{{.5f,-.5f,-.5f},{.5f,.5f,-.5f},{.5f,.5f,.5f},{.5f,-.5f,.5f}}}, {{{-.5f,-.5f,.5f},{-.5f,.5f,.5f},{-.5f,.5f,-.5f},{-.5f,-.5f,-.5f}}}, {{{-.5f,.5f,-.5f},{-.5f,.5f,.5f},{.5f,.5f,.5f},{.5f,.5f,-.5f}}}, {{{-.5f,-.5f,.5f},{-.5f,-.5f,-.5f},{.5f,-.5f,-.5f},{.5f,-.5f,.5f}}}, {{{-.5f,-.5f,.5f},{.5f,-.5f,.5f},{.5f,.5f,.5f},{-.5f,.5f,.5f}}}, {{{.5f,-.5f,-.5f},{-.5f,-.5f,-.5f},{-.5f,.5f,-.5f},{.5f,.5f,-.5f}}}}}; for(unsigned f=0;f<6;++f) { unsigned start=static_cast(m.vertices.size());auto n=normals[f]; for(auto p:faces[f])m.vertices.push_back({p.x,p.y,p.z,n.x,n.y,n.z}); m.indices.insert(m.indices.end(),{start,start+1,start+2,start,start+2,start+3}); } return m; } Vec3 sub(Vec3 a,Vec3 b) {return {a.x-b.x,a.y-b.y,a.z-b.z};} float length(Vec3 a) {return std::sqrt(a.x*a.x+a.y*a.y+a.z*a.z);} Vec3 pressure_color(float pressure) {return pressure_tint(pressure);} void color(Vec3 c) {glColor3f(c.x,c.y,c.z);} struct Route {std::array points{};std::array fractions{};unsigned count{};}; // Scene points specify a fraction of the pipe's physical length, converted // to metre-based PipeAnchor distances when constructing the cached surface. Route route(std::initializer_list p) { Route r;for(auto v:p) {r.points[r.count]=v.position;r.fractions[r.count++]=v.distance;}return r; } Vec3 cross(Vec3 a,Vec3 b) {return {a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x};} float dot(Vec3 a,Vec3 b) {return a.x*b.x+a.y*b.y+a.z*b.z;} Vec3 unit(Vec3 a) {float l=length(a);return l>1e-7f?Vec3{a.x/l,a.y/l,a.z/l}:Vec3{1,0,0};} struct PipeVisual { struct Ring {unsigned first;std::size_t a,b;float weight;}; Mesh mesh; std::vector colors; std::vector rings; std::vector lengths,areas; Route route; bool valid{}; bool matches(const ScenarioSnapshot& s,std::size_t index,const Route& r) const { if(!valid || lengths!=s.pipe_lengths[index] || areas!=s.pipe_areas[index] || r.count!=route.count)return false; for(unsigned i=0;i1e-6f || r.fractions[i]!=route.fractions[i])return false; return true; } void build(const ScenarioSnapshot& s,std::size_t index,const Route& r) { lengths=s.pipe_lengths[index];areas=s.pipe_areas[index];route=r; mesh={};rings.clear(); std::vector centers,samples{0};float total=0; for(float l:lengths) {centers.push_back(total+l*.5f);samples.push_back(total+l*.5f);total+=l;samples.push_back(total);} std::vector anchors; for(unsigned i=0;i(std::upper_bound(centers.begin(),centers.end(),d)-centers.begin(),centers.size()-1); std::size_t a=b?b-1:0; const float blend=a==b?0:std::clamp((d-centers[a])/(centers[b]-centers[a]),0.f,1.f); const float radius=std::sqrt((areas[a]+(areas[b]-areas[a])*blend)/pi3); const unsigned first=static_cast(mesh.vertices.size()); if(!rings.empty())for(unsigned j=0;j& states) { constexpr unsigned sides=20; for(auto ring:rings) { const float p=states[ring.a].pressure+(states[ring.b].pressure-states[ring.a].pressure)*ring.weight; std::fill_n(colors.begin()+ring.first,sides,pressure_tint(p)); } } }; } struct EngineRenderer3D::Impl { Mesh tube=cylinder(),shell=cylinder(true),block=cube(); std::array pipes; void rod(Vec3 a,Vec3 b,float radius,Vec3 tint) const { auto d=sub(b,a);float len=length(d);if(len<1e-6)return; glPushMatrix();glTranslatef(a.x,a.y,a.z); const float angle=std::acos(std::clamp(d.y/len,-1.f,1.f))*180/pi3; if(std::abs(d.x)+std::abs(d.z)>.00001f)glRotatef(angle,d.z,0,-d.x); else if(d.y<0)glRotatef(180,1,0,0); glScalef(radius,len,radius);color(tint);tube.draw();glPopMatrix(); } void box(Vec3 center,Vec3 scale,Vec3 tint) const {glPushMatrix();glTranslatef(center.x,center.y,center.z);glScalef(scale.x,scale.y,scale.z);color(tint);block.draw();glPopMatrix();} void pipe(const ScenarioSnapshot& s,std::size_t index,const Route& r) { if(s.pipe_states[index].empty())return; auto& visual=pipes[index]; if(!visual.matches(s,index,r))visual.build(s,index,r); visual.update(s.pipe_states[index]); visual.mesh.draw(&visual.colors); } void cylinder_part(Vec3 center,float fraction,float pressure,float angle,float tilt=0) const { glPushMatrix();glTranslatef(center.x,.1f,center.z);glRotatef(tilt,1,0,0);glTranslatef(-center.x,-.1f,-center.z); const float piston=.43f-.18f*std::clamp(fraction,0.f,1.f); glPushMatrix();glTranslatef(center.x,.19f,center.z);glScalef(.059f,.29f,.059f);color({.4f,.4f,.4f});shell.draw();glPopMatrix(); rod({center.x,piston+.025f,center.z},{center.x,.47f,center.z},.047f,pressure_color(pressure)); rod({center.x,piston,center.z},{center.x,piston+.025f,center.z},.05f,{.8f,.8f,.8f}); Vec3 pin{center.x,.1f+.055f*std::cos(angle),center.z+.055f*std::sin(angle)}; rod(pin,{center.x,piston,center.z},.012f,{.65f,.65f,.65f}); rod({center.x,.1f,center.z},pin,.019f,{.42f,.42f,.42f}); box({center.x,.487f,center.z},{.12f,.028f,.12f},{.48f,.48f,.48f}); glPopMatrix(); } void engine(const ScenarioSnapshot& s) { if(s.two_stroke) { cylinder_part({0,0,0},s.cylinder_piston_fraction1,s.cylinder_pressure1,s.cylinder_angles[0]); box({0,.07f,0},{.21f,.14f,.19f},pressure_color(s.crankcase_pressure)); box({-.55f,.35f,0},{.18f,.12f,.15f},pressure_color(s.intake_pressure)); pipe(s,0,route({{0.0f,{-1.05f,.35f,0}},{1.0f,{-.64f,.35f,0}}})); pipe(s,1,route({{0.0f,{-.46f,.35f,0}},{0.3333333333333333f,{-.28f,.35f,0}},{0.6666666666666666f,{-.28f,.1f,0}},{1.0f,{-.105f,.1f,0}}})); pipe(s,2,route({{0.0f,{.105f,.09f,0}},{0.3333333333333333f,{.18f,.09f,-.12f}},{0.6666666666666666f,{.18f,.35f,-.12f}},{1.0f,{.05f,.35f,0}}})); pipe(s,3,route({{0.0f,{.055f,.40f,0}},{0.3333333333333333f,{.3f,.40f,0}},{0.6666666666666666f,{.58f,.23f,0}},{1.0f,{1.45f,.23f,0}}})); } else { const int n=s.cylinder_count;const bool v8=s.v8_headers; const bool banked=s.layout!=EngineLayout::Inline;const int per_bank=banked?n/2:n; const float spacing=.21f,first=-(per_bank-1)*spacing*.5f; box({0,.64f,0},{per_bank*.21f,.12f,.15f},pressure_color(s.intake_pressure)); pipe(s,0,route({{0.0f,{-1.05f,.64f,0}},{0.25f,{-.5f,.64f,0}},{0.5f,{-.5f,.64f,-.1f}},{0.75f,{0,.64f,-.1f}},{1.0f,{0,.64f,0}}})); for(int i=0;i pair{0,0,1,1,2,3,2,3}; for(int i=0;i<8;++i) { const int bank=i/4, group=pair[i]%2; const float x=first+(i%4)*spacing, z=bank? .39f:-.39f; const float a=(bank?28.f:-28.f)*pi3/180; const Vec3 port{x+.055f,.1f+.33f*std::cos(a),.33f*std::sin(a)}; const float end_z=z+(group? .09f:-.09f); pipe(s,9+i,route({{0,port},{.3f,{x+.12f,.34f,z}},{.65f,{.35f,.20f,end_z}},{1,{.74f,.17f,end_z}}})); } for(int i=0;i<4;++i) { const float z=i<2?-.39f:.39f; pipe(s,17+i,route({{0,{.74f,.17f,z+(i%2?.09f:-.09f)}},{.5f,{.94f,.17f,z+(i%2?.06f:-.06f)}},{1,{1.14f,.17f,z}}})); } if(s.h_pipe) pipe(s,25,route({{0,{1.69f,.17f,-.39f}},{1,{1.69f,.17f,.39f}}})); } for(int bank=0;bank<(v8?2:1);++bank) { float z=v8?(bank==0?-.39f:.39f):.26f; if(!v8) box({.68f,.17f,z},{.12f,.10f,.12f},pressure_color(bank==0?s.exhaust_pressure:s.second_bank_pressure)); if(s.silencers) { const auto& c=s.exhaust_config; const float total=c.inlet_length+c.resonator.length+c.connecting_length+c.muffler.length+c.outlet_length; const float start=.74f; pipe(s,1+2*n,route({{0,{start,.17f,z}},{1,{start+total,.17f,z}}})); float x=start+c.inlet_length; for(int index=0;index<2;++index) { if(index) x+=c.connecting_length; const auto& silencer=index?c.muffler:c.resonator; rod({x,.17f,z},{x+silencer.length,.17f,z},silencer.diameter()*.505f,{.45f,.45f,.45f}); x+=silencer.length; } } else if(v8) { pipe(s,21+bank,route({{0,{1.14f,.17f,z}},{1,{1.69f,.17f,z}}})); pipe(s,23+bank,route({{0,{1.69f,.17f,z}},{1,{2.94f,.17f,z}}})); } else pipe(s,1+2*n,route({{0,{.74f,.17f,z}},{1,{1.45f,.17f,z}}})); if(bank==0)rod({first-.07f,.1f,0},{first+(per_bank-1)*spacing+.07f,.1f,0},.022f,{.56f,.56f,.56f}); } } } }; EngineRenderer3D::EngineRenderer3D():impl_(std::make_unique()){} EngineRenderer3D::~EngineRenderer3D()=default; void EngineRenderer3D::draw(sf::RenderWindow& window,const ScenarioSnapshot& state,sf::FloatRect viewport,const OrbitCamera& camera) { window.pushGLStates(); const auto size=window.getSize(); const int x=int(viewport.position.x),y=int(float(size.y)-viewport.position.y-viewport.size.y),w=int(viewport.size.x),h=int(viewport.size.y); glViewport(x,y,w,h);glEnable(GL_SCISSOR_TEST);glScissor(x,y,w,h);glClear(GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LEQUAL);glDisable(GL_BLEND);glDisable(GL_TEXTURE_2D);glDisable(GL_CULL_FACE); glEnable(GL_NORMALIZE);glEnable(GL_LIGHTING);glEnable(GL_LIGHT0);glEnable(GL_COLOR_MATERIAL);glColorMaterial(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE); glMatrixMode(GL_PROJECTION);glLoadIdentity();const double near=.03,top=near*.41421356; glFrustum(-top*w/h,top*w/h,-top,top,near,50); glMatrixMode(GL_MODELVIEW);glLoadIdentity(); const GLfloat light[]{-.4f,1,1,0},ambient[]{.35f,.35f,.35f,1},diffuse[]{1,1,1,1}; glLightfv(GL_LIGHT0,GL_POSITION,light);glLightfv(GL_LIGHT0,GL_DIFFUSE,diffuse);glLightModelfv(GL_LIGHT_MODEL_AMBIENT,ambient); glTranslatef(0,0,-camera.distance*(state.silencers?1.8f:state.cylinder_count==8?1.4f:1.f));glRotatef(camera.pitch,1,0,0);glRotatef(camera.yaw,0,1,0);glTranslatef(-camera.target.x-(state.silencers?1.1f:state.cylinder_count==8?.7f:0.f),-camera.target.y,-camera.target.z); // Grid is indexed triangles too; no per-frame geometry allocation. for(int i=-8;i<=8;++i) { float p=i*.2f; impl_->box({p,-.025f,0},{.0015f,.001f,3.2f},{.16f,.16f,.16f}); impl_->box({0,-.025f,p},{3.2f,.001f,.0015f},{.16f,.16f,.16f}); } impl_->engine(state); glDisable(GL_SCISSOR_TEST);window.popGLStates(); } }