Files
FluidSimCpp/src/engine_renderer_3d.cpp
2026-10-23 18:29:51 +02:00

255 lines
15 KiB
C++

#include "FluidSim/Desktop/EngineRenderer3D.hpp"
#include "FluidSim/Desktop/PressurePalette.hpp"
#include "FluidSim/Desktop/PipePath.hpp"
#include <SFML/OpenGL.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <vector>
namespace fluidsim::port::desktop {
namespace {
constexpr float pi3=3.14159265359f;
struct Vertex {float x,y,z,nx,ny,nz;};
struct Mesh {
std::vector<Vertex> vertices;
std::vector<unsigned> indices;
void draw(const std::vector<Vec3>* 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<const char*>(vertices.data())+offsetof(Vertex,nx));
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(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<sides) {unsigned k=2*i;m.indices.insert(m.indices.end(),{k,k+1,k+2,k+2,k+1,k+3});}
}
if(!half) for(unsigned cap=0;cap<2;++cap) {
unsigned center=static_cast<unsigned>(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<sides;++i) m.indices.insert(m.indices.end(),{center,center+1+i,center+2+i});
}
return m;
}
Mesh cube() {
Mesh m;
const std::array<Vec3,6> normals{{{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}};
const std::array<std::array<Vec3,4>,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<unsigned>(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<Vec3,5> points{};std::array<float,5> 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<PipeAnchor> 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<Vec3> colors;
std::vector<Ring> rings;
std::vector<float> 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;i<r.count;++i)if(length(sub(r.points[i],route.points[i]))>1e-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<float> 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<PipeAnchor> anchors;
for(unsigned i=0;i<r.count;++i) {
const float distance=total*r.fractions[i];
anchors.push_back({distance,r.points[i]});samples.push_back(distance);
}
PipePath path(std::move(anchors));
std::sort(samples.begin(),samples.end());samples.erase(std::unique(samples.begin(),samples.end(),[](float a,float b){return std::abs(a-b)<1e-7f;}),samples.end());
constexpr unsigned sides=20;
Vec3 normal{};
for(float d:samples) {
const auto position=path.position(d),tangent=path.tangent(d);
if(rings.empty())normal=unit(cross(tangent,std::abs(tangent.y)<.9f?Vec3{0,1,0}:Vec3{1,0,0}));
else {float v=dot(normal,tangent);normal=unit({normal.x-v*tangent.x,normal.y-v*tangent.y,normal.z-v*tangent.z});}
const auto binormal=unit(cross(tangent,normal));
std::size_t b=std::min<std::size_t>(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<unsigned>(mesh.vertices.size());
if(!rings.empty())for(unsigned j=0;j<sides;++j) {
unsigned prev=first-sides,k=(j+1)%sides;
mesh.indices.insert(mesh.indices.end(),{prev+j,first+j,prev+k,prev+k,first+j,first+k});
}
rings.push_back({first,a,b,blend});
for(unsigned j=0;j<sides;++j) {
float angle=2*pi3*j/sides,c=std::cos(angle),sn=std::sin(angle);
Vec3 n{normal.x*c+binormal.x*sn,normal.y*c+binormal.y*sn,normal.z*c+binormal.z*sn};
mesh.vertices.push_back({position.x+radius*n.x,position.y+radius*n.y,position.z+radius*n.z,n.x,n.y,n.z});
}
}
colors.resize(mesh.vertices.size());valid=true;
}
void update(const std::vector<GasState>& 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<PipeVisual,32> 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<n;++i) {
const float x=first+(i%per_bank)*spacing,z=0;
const float bank_tilt=s.layout==EngineLayout::Boxer?90.f:s.bank_angle*.5f;
const float tilt=banked?(i<per_bank?-bank_tilt:bank_tilt):0;
const auto port=[&](float y,float dx) {float a=tilt*pi3/180;return Vec3{x+dx,.1f+(y-.1f)*std::cos(a),z+(y-.1f)*std::sin(a)};};
cylinder_part({x,0,z},s.piston_fractions[i],s.cylinder_pressures[i],s.cylinder_angles[i],tilt);
const float cz=v8?(i<4?-.39f:.39f):.26f;
const auto intake=port(.45f,-.055f),exhaust=port(.43f,.055f);
pipe(s,1+i,route({{0,{x,.58f,0}},{.3f,{x,.55f,intake.z*.5f}},{.7f,{intake.x,.52f,intake.z}},{1,intake}}));
if(!v8) pipe(s,1+n+i,route({{0,exhaust},{.3f,{x+.12f,exhaust.y,cz}},{.6f,{x+.12f,.17f,cz}},{1,{.68f,.17f,cz}}}));
}
if(v8) {
const std::array<int,8> 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<Impl>()){}
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();
}
}