GPU particle collider support (#529)

This commit is contained in:
Turánszki János
2022-08-24 14:52:13 +02:00
committed by GitHub
parent 092bb536bd
commit 8aad16113d
16 changed files with 348 additions and 73 deletions
Binary file not shown.
+1
View File
@@ -37,6 +37,7 @@ void ColliderWindow::Create(EditorComponent* _editor)
shapeCombo.SetPos(XMFLOAT2(x, y));
shapeCombo.AddItem("Sphere", (uint64_t)ColliderComponent::Shape::Sphere);
shapeCombo.AddItem("Capsule", (uint64_t)ColliderComponent::Shape::Capsule);
shapeCombo.AddItem("Plane", (uint64_t)ColliderComponent::Shape::Plane);
shapeCombo.OnSelect([=](wi::gui::EventArgs args) {
wi::scene::Scene& scene = editor->GetCurrentScene();
ColliderComponent* collider = scene.colliders.GetComponent(entity);
+1 -1
View File
@@ -1001,7 +1001,7 @@ void EditorComponent::Update(float dt)
optionsWnd.RefreshEntityTree();
}
else
else if(translator.selected.empty())
{
// Check for interactive grass (hair particle that is child of hovered object:
for (size_t i = 0; i < scene.hairs.GetCount(); ++i)
+1
View File
@@ -54,6 +54,7 @@
#define ICON_PAUSE ICON_FA_PAUSE
#define ICON_STOP ICON_FA_STOP
#define ICON_PEN ICON_FA_PEN
#define ICON_FILTER ICON_FA_FILTER
#define ICON_DARK ICON_FA_MOON
#define ICON_BRIGHT ICON_FA_SUN
+1 -1
View File
@@ -289,7 +289,7 @@ void OptionsWindow::Create(EditorComponent* _editor)
filterCombo.Create("Filter: ");
filterCombo.AddItem("All ", (uint64_t)Filter::All);
filterCombo.AddItem("All " ICON_FILTER, (uint64_t)Filter::All);
filterCombo.AddItem("Transform " ICON_TRANSFORM, (uint64_t)Filter::Transform);
filterCombo.AddItem("Material " ICON_MATERIAL, (uint64_t)Filter::Material);
filterCombo.AddItem("Mesh " ICON_MESH, (uint64_t)Filter::Mesh);
+24 -8
View File
@@ -515,6 +515,10 @@ struct ShaderEntity
{
return GetConeAngleCos();
}
inline float3 GetColliderTip()
{
return shadowAtlasMulAdd.xyz;
}
#else
// Application-side:
@@ -572,6 +576,10 @@ struct ShaderEntity
{
SetConeAngleCos(value);
}
inline void SetColliderTip(float3 value)
{
shadowAtlasMulAdd = float4(value.x, value.y, value.z, 0);
}
#endif // __cplusplus
};
@@ -607,13 +615,21 @@ struct ShaderFrustum
#endif // __cplusplus
};
static const uint ENTITY_TYPE_DIRECTIONALLIGHT = 0;
static const uint ENTITY_TYPE_POINTLIGHT = 1;
static const uint ENTITY_TYPE_SPOTLIGHT = 2;
static const uint ENTITY_TYPE_DECAL = 100;
static const uint ENTITY_TYPE_ENVMAP = 101;
static const uint ENTITY_TYPE_FORCEFIELD_POINT = 200;
static const uint ENTITY_TYPE_FORCEFIELD_PLANE = 201;
enum SHADER_ENTITY_TYPE
{
ENTITY_TYPE_DIRECTIONALLIGHT,
ENTITY_TYPE_POINTLIGHT,
ENTITY_TYPE_SPOTLIGHT,
ENTITY_TYPE_DECAL,
ENTITY_TYPE_ENVMAP,
ENTITY_TYPE_FORCEFIELD_POINT,
ENTITY_TYPE_FORCEFIELD_PLANE,
ENTITY_TYPE_COLLIDER_SPHERE,
ENTITY_TYPE_COLLIDER_CAPSULE,
ENTITY_TYPE_COLLIDER_PLANE,
ENTITY_TYPE_COUNT
};
static const uint ENTITY_FLAG_LIGHT_STATIC = 1 << 0;
@@ -688,7 +704,7 @@ struct FrameCB
uint forcefieldarray_count; // indexing into entity array
uint envprobearray_offset; // indexing into entity array
uint envprobearray_count; // indexing into entity array
uint envprobearray_count; // indexing into entity array
uint temporalaa_samplerotation;
float blue_noise_phase;
@@ -21,8 +21,8 @@ RWByteAddressBuffer vertexBuffer_COL : register(u9);
RWStructuredBuffer<uint> culledIndirectionBuffer : register(u10);
RWStructuredBuffer<uint> culledIndirectionBuffer2 : register(u11);
#define SPH_FLOOR_COLLISION
#define SPH_BOX_COLLISION
//#define SPH_FLOOR_COLLISION
//#define SPH_BOX_COLLISION
[numthreads(THREADCOUNT_SIMULATION, 1, 1)]
@@ -40,29 +40,105 @@ void main(uint3 DTid : SV_DispatchThreadID, uint Gid : SV_GroupIndex)
Particle particle = particleBuffer[particleIndex];
uint v0 = particleIndex * 4;
const float lifeLerp = 1 - particle.life / particle.maxLife;
const float particleSize = lerp(particle.sizeBeginEnd.x, particle.sizeBeginEnd.y, lifeLerp);
// integrate:
particle.force += xParticleGravity;
particle.velocity += particle.force * dt;
particle.position += particle.velocity * dt;
// reset force for next frame:
particle.force = 0;
// drag:
particle.velocity *= xParticleDrag;
if (particle.life > 0)
{
// simulate:
// process forces and colliders:
for (uint i = 0; i < GetFrame().forcefieldarray_count; ++i)
{
ShaderEntity forceField = load_entity(GetFrame().forcefieldarray_offset + i);
ShaderEntity entity = load_entity(GetFrame().forcefieldarray_offset + i);
[branch]
if (forceField.layerMask & xEmitterLayerMask)
if (entity.layerMask & xEmitterLayerMask)
{
float3 dir = forceField.position - particle.position;
float dist;
if (forceField.GetType() == ENTITY_TYPE_FORCEFIELD_POINT) // point-based force field
{
dist = length(dir);
}
else // planar force field
{
dist = dot(forceField.GetDirection(), dir);
dir = forceField.GetDirection();
}
const float range = entity.GetRange();
const uint type = entity.GetType();
particle.force += dir * forceField.GetGravity() * (1 - saturate(dist / forceField.GetRange()));
if (type == ENTITY_TYPE_COLLIDER_CAPSULE)
{
float3 A = entity.position;
float3 B = entity.GetColliderTip();
float3 N = normalize(A - B);
A -= N * range;
B += N * range;
float3 C = closest_point_on_segment(A, B, particle.position);
float3 dir = C - particle.position;
float dist = length(dir);
dir /= dist;
dist = dist - range - particleSize;
if (dist < 0)
{
particle.velocity = reflect(particle.velocity, dir);
float3 offset_velocity = dir * dist;
particle.position = particle.position + offset_velocity;
particle.velocity += offset_velocity;
}
}
else
{
float3 dir = entity.position - particle.position;
float dist = length(dir);
dir /= dist;
switch (type)
{
case ENTITY_TYPE_FORCEFIELD_POINT:
particle.force += dir * entity.GetGravity() * (1 - saturate(dist / range));
break;
case ENTITY_TYPE_FORCEFIELD_PLANE:
particle.force += entity.GetDirection() * entity.GetGravity() * (1 - saturate(dist / range));
break;
case ENTITY_TYPE_COLLIDER_SPHERE:
dist = dist - range - particleSize;
if (dist < 0)
{
particle.velocity = reflect(particle.velocity, dir);
float3 offset_velocity = dir * dist;
particle.position = particle.position + offset_velocity;
particle.velocity += offset_velocity;
}
break;
case ENTITY_TYPE_COLLIDER_PLANE:
dir = normalize(entity.GetDirection());
dist = plane_point_distance(entity.position, dir, particle.position);
if (dist < 0)
{
dir *= -1;
dist = abs(dist);
}
dist = dist - particleSize;
if (dist < 0)
{
float4x4 planeProjection = load_entitymatrix(entity.GetMatrixIndex());
const float3 clipSpacePos = mul(planeProjection, float4(particle.position, 1)).xyz;
const float3 uvw = clipspace_to_uv(clipSpacePos.xyz);
[branch]
if (is_saturated(uvw))
{
particle.velocity = reflect(particle.velocity, dir);
float3 offset_velocity = -dir * dist;
particle.position = particle.position + offset_velocity;
particle.velocity += offset_velocity;
}
}
break;
default:
break;
}
}
}
}
@@ -82,9 +158,6 @@ void main(uint3 DTid : SV_DispatchThreadID, uint Gid : SV_GroupIndex)
float surfaceLinearDepth = compute_lineardepth(depth0);
float surfaceThickness = 1.5f;
float lifeLerp = 1 - particle.life / particle.maxLife;
float particleSize = lerp(particle.sizeBeginEnd.x, particle.sizeBeginEnd.y, lifeLerp);
// check if particle is colliding with the depth buffer, but not completely behind it:
if ((pos2D.w + particleSize > surfaceLinearDepth) && (pos2D.w - particleSize < surfaceLinearDepth + surfaceThickness))
{
@@ -106,20 +179,6 @@ void main(uint3 DTid : SV_DispatchThreadID, uint Gid : SV_GroupIndex)
}
#endif // DEPTHCOLLISIONS
// integrate:
particle.force += xParticleGravity;
particle.velocity += particle.force * dt;
particle.position += particle.velocity * dt;
// reset force for next frame:
particle.force = 0;
// drag:
particle.velocity *= xParticleDrag;
float lifeLerp = 1 - particle.life / particle.maxLife;
float particleSize = lerp(particle.sizeBeginEnd.x, particle.sizeBeginEnd.y, lifeLerp);
[branch]
if (xEmitterOptions & EMITTER_OPTION_BIT_SPH_ENABLED)
{
+7
View File
@@ -1020,6 +1020,13 @@ inline float dither(in float2 pixel)
return ditherMask8(pixel);
}
float plane_point_distance(float3 planeOrigin, float3 planeNormal, float3 P)
{
return dot(planeNormal, P - planeOrigin);
}
// o : ray origin
// d : ray direction
// center : sphere center
@@ -116,30 +116,82 @@ void main(uint3 DTid : SV_DispatchThreadID, uint3 Gid : SV_GroupID, uint groupIn
float3 tip = base + normal * len;
// Accumulate forces:
// Accumulate forces, apply colliders:
float3 force = 0;
for (uint i = 0; i < GetFrame().forcefieldarray_count; ++i)
{
ShaderEntity forceField = load_entity(GetFrame().forcefieldarray_offset + i);
ShaderEntity entity = load_entity(GetFrame().forcefieldarray_offset + i);
[branch]
if (forceField.layerMask & xHairLayerMask)
if (entity.layerMask & xHairLayerMask)
{
//float3 dir = forceField.position - PointOnLineSegmentNearestPoint(base, tip, forceField.position);
float3 dir = forceField.position - tip;
float dist;
if (forceField.GetType() == ENTITY_TYPE_FORCEFIELD_POINT) // point-based force field
{
//dist = length(dir);
dist = length(forceField.position - closest_point_on_segment(base, tip, forceField.position));
}
else // planar force field
{
dist = dot(forceField.GetDirection(), dir);
dir = forceField.GetDirection();
}
const float range = entity.GetRange();
const uint type = entity.GetType();
force += dir * forceField.GetGravity() * (1 - saturate(dist / forceField.GetRange()));
if (type == ENTITY_TYPE_COLLIDER_CAPSULE)
{
float3 A = entity.position;
float3 B = entity.GetColliderTip();
float3 N = normalize(A - B);
A -= N * range;
B += N * range;
float3 C = closest_point_on_segment(A, B, tip);
float3 dir = C- tip;
float dist = length(dir);
dir /= dist;
dist = dist - range - len;
if (dist < 0)
{
tip = tip - dir * dist;
}
}
else
{
float3 closest_point = closest_point_on_segment(base, tip, entity.position);
float3 dir = entity.position - closest_point;
float dist = length(dir);
dir /= dist;
switch (type)
{
case ENTITY_TYPE_FORCEFIELD_POINT:
force += dir * entity.GetGravity() * (1 - saturate(dist / range));
break;
case ENTITY_TYPE_FORCEFIELD_PLANE:
force += entity.GetDirection() * entity.GetGravity() * (1 - saturate(dist / range));
break;
case ENTITY_TYPE_COLLIDER_SPHERE:
dist = dist - range - len;
if (dist < 0)
{
tip = tip - dir * dist;
}
break;
case ENTITY_TYPE_COLLIDER_PLANE:
dir = normalize(entity.GetDirection());
dist = plane_point_distance(entity.position, dir, closest_point);
if (dist < 0)
{
dir *= -1;
dist = abs(dist);
}
dist = dist - len;
if (dist < 0)
{
float4x4 planeProjection = load_entitymatrix(entity.GetMatrixIndex());
const float3 clipSpacePos = mul(planeProjection, float4(closest_point, 1)).xyz;
const float3 uvw = clipspace_to_uv(clipSpacePos.xyz);
[branch]
if (is_saturated(uvw))
{
tip = tip + dir * dist;
}
}
break;
default:
break;
}
}
}
}
+9 -3
View File
@@ -404,19 +404,25 @@ namespace wi
params.cursor = wi::font::Draw(infodisplay_str, params, cmd);
if (infoDisplay.vram_usage)
// VRAM:
{
GraphicsDevice::MemoryUsage vram = graphicsDevice->GetMemoryUsage();
bool warn = false;
if (vram.usage > vram.budget)
{
params.color = wi::Color::Error();
warn = true;
}
else if (float(vram.usage) / float(vram.budget) > 0.9f)
{
params.color = wi::Color::Warning();
warn = true;
}
if (infoDisplay.vram_usage || warn)
{
params.cursor = wi::font::Draw("VRAM usage: " + std::to_string(vram.usage / 1024 / 1024) + "MB / " + std::to_string(vram.budget / 1024 / 1024) + "MB\n", params, cmd);
params.color = wi::Color::White();
}
params.cursor = wi::font::Draw("VRAM usage: " + std::to_string(vram.usage / 1024 / 1024) + "MB / " + std::to_string(vram.budget / 1024 / 1024) + "MB\n", params, cmd);
params.color = wi::Color::White();
}
// Write warnings below:
+5
View File
@@ -253,6 +253,11 @@ namespace wi::math
XMVECTOR GetClosestPointToLine(const XMVECTOR& A, const XMVECTOR& B, const XMVECTOR& P, bool segmentClamp = false);
float GetPointSegmentDistance(const XMVECTOR& point, const XMVECTOR& segmentA, const XMVECTOR& segmentB);
inline float GetPlanePointDistance(const XMVECTOR& planeOrigin, const XMVECTOR& planeNormal, const XMVECTOR& point)
{
return XMVectorGetX(XMVector3Dot(planeNormal, point - planeOrigin));
}
float GetAngle(const XMFLOAT2& a, const XMFLOAT2& b);
float GetAngle(const XMFLOAT3& a, const XMFLOAT3& b, const XMFLOAT3& axis);
void ConstructTriangleEquilateral(float radius, XMFLOAT4& A, XMFLOAT4& B, XMFLOAT4& C);
+11 -6
View File
@@ -251,8 +251,13 @@ namespace wi::primitive
}
bool Sphere::intersects(const Sphere& b, float& dist, XMFLOAT3& direction) const
{
dist = wi::math::Distance(center, b.center);
XMStoreFloat3(&direction, (XMLoadFloat3(&center) - XMLoadFloat3(&b.center)) / dist);
XMVECTOR A = XMLoadFloat3(&center);
XMVECTOR B = XMLoadFloat3(&b.center);
XMVECTOR DIR = A - B;
XMVECTOR DIST = XMVector3Length(DIR);
DIR = DIR / DIST;
XMStoreFloat3(&direction, DIR);
dist = XMVectorGetX(DIST);
dist = dist - radius - b.radius;
return dist < 0;
}
@@ -266,8 +271,8 @@ namespace wi::primitive
XMVECTOR A = XMLoadFloat3(&b.base);
XMVECTOR B = XMLoadFloat3(&b.tip);
XMVECTOR N = XMVector3Normalize(A - B);
A += N * b.radius;
B -= N * b.radius;
A -= N * b.radius;
B += N * b.radius;
XMVECTOR C = XMLoadFloat3(&center);
dist = wi::math::GetPointSegmentDistance(C, A, B);
dist = dist - radius - b.radius;
@@ -278,8 +283,8 @@ namespace wi::primitive
XMVECTOR A = XMLoadFloat3(&b.base);
XMVECTOR B = XMLoadFloat3(&b.tip);
XMVECTOR N = XMVector3Normalize(A - B);
A += N * b.radius;
B -= N * b.radius;
A -= N * b.radius;
B += N * b.radius;
XMVECTOR C = XMLoadFloat3(&center);
dist = wi::math::GetPointSegmentDistance(C, A, B);
XMStoreFloat3(&direction, (C - wi::math::ClosestPointOnLineSegment(A, B, C)) / dist);
+82 -2
View File
@@ -3214,7 +3214,7 @@ void UpdatePerFrameData(
frameCB.lightarray_offset = frameCB.envprobearray_offset + frameCB.envprobearray_count;
frameCB.lightarray_count = (uint)vis.visibleLights.size();
frameCB.forcefieldarray_offset = frameCB.lightarray_offset + frameCB.lightarray_count;
frameCB.forcefieldarray_count = (uint)vis.scene->forces.GetCount();
frameCB.forcefieldarray_count = uint(vis.scene->forces.GetCount() + vis.scene->colliders.GetCount());
frameCB.envprobe_mipcount = 0;
frameCB.envprobe_mipcount_rcp = 1.0f;
@@ -3681,6 +3681,57 @@ void UpdateRenderData(
entityCounter++;
}
// Write colliders into entity array:
for (size_t i = 0; i < vis.scene->colliders.GetCount(); ++i)
{
if (entityCounter == SHADER_ENTITY_COUNT)
{
assert(0); // too many entities!
entityCounter--;
break;
}
ShaderEntity shaderentity = {};
const ColliderComponent& collider = vis.scene->colliders[i];
shaderentity.layerMask = ~0u;
Entity entity = vis.scene->colliders.GetEntity(i);
const LayerComponent* layer = vis.scene->layers.GetComponent(entity);
if (layer != nullptr)
{
shaderentity.layerMask = layer->layerMask;
}
switch (collider.shape)
{
case ColliderComponent::Shape::Sphere:
shaderentity.SetType(ENTITY_TYPE_COLLIDER_SPHERE);
shaderentity.position = collider.sphere.center;
shaderentity.SetRange(collider.sphere.radius);
break;
case ColliderComponent::Shape::Capsule:
shaderentity.SetType(ENTITY_TYPE_COLLIDER_CAPSULE);
shaderentity.position = collider.capsule.base;
shaderentity.SetColliderTip(collider.capsule.tip);
shaderentity.SetRange(collider.capsule.radius);
break;
case ColliderComponent::Shape::Plane:
shaderentity.SetType(ENTITY_TYPE_COLLIDER_PLANE);
shaderentity.position = collider.planeOrigin;
shaderentity.SetDirection(collider.planeNormal);
shaderentity.SetIndices(matrixCounter, ~0u);
std::memcpy(&matrixArray[matrixCounter++], &collider.planeProjection, sizeof(collider.planeProjection));
break;
default:
assert(0);
break;
}
std::memcpy(entityArray + entityCounter, &shaderentity, sizeof(ShaderEntity));
entityCounter++;
}
// Write force fields into entity array:
for (size_t i = 0; i < vis.scene->forces.GetCount(); ++i)
{
@@ -5103,7 +5154,36 @@ void DrawDebugWorld(
DrawSphere(collider.sphere, XMFLOAT4(1, 0, 1, 1));
break;
case ColliderComponent::Shape::Capsule:
DrawCapsule(collider.capsule, XMFLOAT4(1, 1, 0, 1));
DrawCapsule(collider.capsule, XMFLOAT4(1, 0, 1, 1));
break;
case ColliderComponent::Shape::Plane:
{
RenderableLine line;
line.color_start = XMFLOAT4(1, 0, 1, 1);
line.color_end = XMFLOAT4(1, 0, 1, 1);
XMMATRIX planeMatrix = XMMatrixInverse(nullptr, XMLoadFloat4x4(&collider.planeProjection));
XMVECTOR P0 = XMVector3Transform(XMVectorSet(-1, 0, -1, 1), planeMatrix);
XMVECTOR P1 = XMVector3Transform(XMVectorSet(1, 0, -1, 1), planeMatrix);
XMVECTOR P2 = XMVector3Transform(XMVectorSet(1, 0, 1, 1), planeMatrix);
XMVECTOR P3 = XMVector3Transform(XMVectorSet(-1, 0, 1, 1), planeMatrix);
XMStoreFloat3(&line.start, P0);
XMStoreFloat3(&line.end, P1);
DrawLine(line);
XMStoreFloat3(&line.start, P1);
XMStoreFloat3(&line.end, P2);
DrawLine(line);
XMStoreFloat3(&line.start, P2);
XMStoreFloat3(&line.end, P3);
DrawLine(line);
XMStoreFloat3(&line.start, P3);
XMStoreFloat3(&line.end, P0);
DrawLine(line);
XMVECTOR O = XMLoadFloat3(&collider.planeOrigin);
XMVECTOR N = XMLoadFloat3(&collider.planeNormal);
XMStoreFloat3(&line.start, O);
XMStoreFloat3(&line.end, O + N);
DrawLine(line);
}
break;
}
}
+39
View File
@@ -3196,6 +3196,20 @@ namespace wi::scene
tail -= N * collider.capsule.radius;
XMStoreFloat3(&collider.capsule.base, offset);
XMStoreFloat3(&collider.capsule.tip, tail);
if (collider.shape == ColliderComponent::Shape::Plane)
{
collider.planeOrigin = collider.sphere.center;
XMVECTOR N = XMVectorSet(0, 1, 0, 0);
N = XMVector3Normalize(XMVector3TransformNormal(N, W));
XMStoreFloat3(&collider.planeNormal, N);
XMMATRIX PLANE = XMMatrixScaling(collider.radius, 1, collider.radius);
PLANE = PLANE * XMMatrixTranslationFromVector(XMLoadFloat3(&collider.offset));
PLANE = PLANE * W;
PLANE = XMMatrixInverse(nullptr, PLANE);
XMStoreFloat4x4(&collider.planeProjection, PLANE);
}
}
}
void Scene::RunSpringUpdateSystem(wi::jobsystem::context& ctx)
@@ -3347,6 +3361,31 @@ namespace wi::scene
case ColliderComponent::Shape::Capsule:
tail_sphere.intersects(collider.capsule, dist, direction);
break;
case ColliderComponent::Shape::Plane:
dist = wi::math::GetPlanePointDistance(XMLoadFloat3(&collider.planeOrigin), XMLoadFloat3(&collider.planeNormal), tail_next);
direction = collider.planeNormal;
if (dist < 0)
{
direction.x *= -1;
direction.y *= -1;
direction.z *= -1;
dist = std::abs(dist);
}
dist = dist - tail_sphere.radius;
if (dist < 0)
{
XMMATRIX planeProjection = XMLoadFloat4x4(&collider.planeProjection);
XMVECTOR clipSpacePos = XMVector3Transform(tail_next, planeProjection);
XMVECTOR uvw = clipSpacePos * XMVectorSet(0.5f, -0.5f, 0.5f, 1) + XMVectorSet(0.5f, 0.5f, 0.5f, 0);
XMVECTOR uvw_sat = XMVectorSaturate(uvw);
if (std::abs(XMVectorGetX(uvw) - XMVectorGetX(uvw_sat)) > std::numeric_limits<float>::epsilon())
dist = 1; // force no collision
else if (std::abs(XMVectorGetY(uvw) - XMVectorGetY(uvw_sat)) > std::numeric_limits<float>::epsilon())
dist = 1; // force no collision
else if (std::abs(XMVectorGetZ(uvw) - XMVectorGetZ(uvw_sat)) > std::numeric_limits<float>::epsilon())
dist = 1; // force no collision
}
break;
}
if (dist < 0)
+4
View File
@@ -1327,6 +1327,7 @@ namespace wi::scene
{
Sphere,
Capsule,
Plane,
};
Shape shape;
@@ -1337,6 +1338,9 @@ namespace wi::scene
// Non-serialized attributes:
wi::primitive::Sphere sphere;
wi::primitive::Capsule capsule;
XMFLOAT3 planeOrigin = {};
XMFLOAT3 planeNormal = {};
XMFLOAT4X4 planeProjection = wi::math::IDENTITY_MATRIX;
void Serialize(wi::Archive& archive, wi::ecs::EntitySerializer& seri);
};
+1 -1
View File
@@ -9,7 +9,7 @@ namespace wi::version
// minor features, major updates, breaking compatibility changes
const int minor = 71;
// minor bug fixes, alterations, refactors, updates
const int revision = 24;
const int revision = 25;
const std::string version_string = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(revision);