Initial Commit

This commit is contained in:
Lwmte 2025-04-25 21:03:32 +02:00
parent 6c0eeff41c
commit 1089ef9463
4 changed files with 109 additions and 34 deletions

View file

@ -8,6 +8,7 @@ TombEngine releases are located in this repository (alongside with Tomb Editor):
## New features ## New features
* Added video playback support. * Added video playback support.
* Added muzzle glow effect for firearms. * Added muzzle glow effect for firearms.
* Added weather particle clustering and increase weather particle performance.
### Bug fixes ### Bug fixes
* Fixed Teleporter object. * Fixed Teleporter object.
@ -25,7 +26,6 @@ TombEngine releases are located in this repository (alongside with Tomb Editor):
* Fixed crashes when Lara is on a vehicle unreachable by friendly NPCs. * Fixed crashes when Lara is on a vehicle unreachable by friendly NPCs.
* Removed legacy TR5 search object code which caused issues with meshswaps. * Removed legacy TR5 search object code which caused issues with meshswaps.
* Removed excessive HK nerfing in running state. * Removed excessive HK nerfing in running state.
* Optimized weather particle rendering.
### Lua API changes ### Lua API changes
* Added `View.PlayVideo`, `View.StopVideo`, and other helper functions for the video playback. * Added `View.PlayVideo`, `View.StopVideo`, and other helper functions for the video playback.

View file

@ -433,13 +433,17 @@ namespace TEN::Effects::Environment
// Produce ripples if particle got into substance (water or swamp). // Produce ripples if particle got into substance (water or swamp).
if (inSubstance) if (inSubstance)
SpawnRipple(part.Position, part.RoomNumber, Random::GenerateFloat(16.0f, 24.0f), (int)RippleFlags::SlowFade | (int)RippleFlags::LowOpacity); {
auto ripplePos = part.Position;
ripplePos.y = pointColl.GetWaterSurfaceHeight();
SpawnRipple(ripplePos, part.RoomNumber, Random::GenerateFloat(16.0f, 24.0f), (int)RippleFlags::SlowFade | (int)RippleFlags::LowOpacity);
}
// Immediately disable rain particle because it doesn't need fading out. // Immediately disable rain particle because it doesn't need fading out.
if (part.Type == WeatherType::Rain) if (part.Type == WeatherType::Rain)
{ {
part.Enabled = false; part.Enabled = false;
AddWaterSparks(prevPos.x, prevPos.y, prevPos.z, 6); AddWaterSparks(prevPos.x, inSubstance ? pointColl.GetWaterSurfaceHeight() : pointColl.GetFloorHeight() - 32, prevPos.z, 6);
} }
continue; continue;
@ -587,7 +591,7 @@ namespace TEN::Effects::Environment
auto xPos = Camera.pos.x + ((int)(phd_cos(angle) * radius)); auto xPos = Camera.pos.x + ((int)(phd_cos(angle) * radius));
auto zPos = Camera.pos.z + ((int)(phd_sin(angle) * radius)); auto zPos = Camera.pos.z + ((int)(phd_sin(angle) * radius));
auto yPos = Camera.pos.y - (BLOCK(4) + Random::GenerateInt() & (BLOCK(4) - 1)); auto yPos = Camera.pos.y - (BLOCK(3) + Random::GenerateInt() & (BLOCK(4) - 1));
auto outsideRoom = IsRoomOutside(xPos, yPos, zPos); auto outsideRoom = IsRoomOutside(xPos, yPos, zPos);
@ -607,12 +611,14 @@ namespace TEN::Effects::Environment
switch (level.GetWeatherType()) switch (level.GetWeatherType())
{ {
case WeatherType::Snow: case WeatherType::Snow:
part.ClusterSize = (int)(level.GetWeatherStrength() * WEATHER_PARTICLE_CLUSTER_MULT / 2);
part.Size = Random::GenerateFloat(SNOW_SIZE_MAX / 3, SNOW_SIZE_MAX); part.Size = Random::GenerateFloat(SNOW_SIZE_MAX / 3, SNOW_SIZE_MAX);
part.Velocity.y = Random::GenerateFloat(SNOW_VELOCITY_MAX / 4, SNOW_VELOCITY_MAX) * (part.Size / SNOW_SIZE_MAX); part.Velocity.y = Random::GenerateFloat(SNOW_VELOCITY_MAX / 4, SNOW_VELOCITY_MAX) * (part.Size / SNOW_SIZE_MAX);
part.Life = (SNOW_VELOCITY_MAX / 3) + ((SNOW_VELOCITY_MAX / 2) - ((int)part.Velocity.y >> 2)); part.Life = (SNOW_VELOCITY_MAX / 3) + ((SNOW_VELOCITY_MAX / 2) - ((int)part.Velocity.y >> 2));
break; break;
case WeatherType::Rain: case WeatherType::Rain:
part.ClusterSize = (int)(level.GetWeatherStrength() * WEATHER_PARTICLE_CLUSTER_MULT);
part.Size = Random::GenerateFloat(RAIN_SIZE_MAX / 2, RAIN_SIZE_MAX); part.Size = Random::GenerateFloat(RAIN_SIZE_MAX / 2, RAIN_SIZE_MAX);
part.Velocity.y = Random::GenerateFloat(RAIN_VELOCITY_MAX / 2, RAIN_VELOCITY_MAX) * (part.Size / RAIN_SIZE_MAX) * std::clamp(level.GetWeatherStrength(), 0.6f, 1.0f); part.Velocity.y = Random::GenerateFloat(RAIN_VELOCITY_MAX / 2, RAIN_VELOCITY_MAX) * (part.Size / RAIN_SIZE_MAX) * std::clamp(level.GetWeatherStrength(), 0.6f, 1.0f);
part.Life = (RAIN_VELOCITY_MAX * 2) - part.Velocity.y; part.Life = (RAIN_VELOCITY_MAX * 2) - part.Velocity.y;
@ -622,6 +628,7 @@ namespace TEN::Effects::Environment
part.Velocity.x = Random::GenerateFloat(WEATHER_PARTICLE_HORIZONTAL_VELOCITY / 2, WEATHER_PARTICLE_HORIZONTAL_VELOCITY); part.Velocity.x = Random::GenerateFloat(WEATHER_PARTICLE_HORIZONTAL_VELOCITY / 2, WEATHER_PARTICLE_HORIZONTAL_VELOCITY);
part.Velocity.z = Random::GenerateFloat(WEATHER_PARTICLE_HORIZONTAL_VELOCITY / 2, WEATHER_PARTICLE_HORIZONTAL_VELOCITY); part.Velocity.z = Random::GenerateFloat(WEATHER_PARTICLE_HORIZONTAL_VELOCITY / 2, WEATHER_PARTICLE_HORIZONTAL_VELOCITY);
part.UniqueID = (int)Particles.size();
part.Type = level.GetWeatherType(); part.Type = level.GetWeatherType();
part.RoomNumber = outsideRoom; part.RoomNumber = outsideRoom;
part.Position.x = xPos; part.Position.x = xPos;

View file

@ -9,6 +9,7 @@ using namespace TEN::Entities::Effects;
namespace TEN::Effects::Environment namespace TEN::Effects::Environment
{ {
constexpr auto WEATHER_PARTICLE_SPAWN_DENSITY = 32; constexpr auto WEATHER_PARTICLE_SPAWN_DENSITY = 32;
constexpr auto WEATHER_PARTICLE_CLUSTER_MULT = 16.0f;
constexpr auto WEATHER_PARTICLE_COUNT_MAX = 4096; constexpr auto WEATHER_PARTICLE_COUNT_MAX = 4096;
constexpr auto WEATHER_PARTICLE_COLL_CHECK_DELAY_MAX = 5.0f; constexpr auto WEATHER_PARTICLE_COLL_CHECK_DELAY_MAX = 5.0f;
@ -70,6 +71,7 @@ namespace TEN::Effects::Environment
struct WeatherParticle struct WeatherParticle
{ {
WeatherType Type = WeatherType::None; WeatherType Type = WeatherType::None;
int UniqueID = 0;
Vector3 Position = Vector3::Zero; Vector3 Position = Vector3::Zero;
int RoomNumber = NO_VALUE; int RoomNumber = NO_VALUE;
@ -79,6 +81,7 @@ namespace TEN::Effects::Environment
float Life = 0.0f; float Life = 0.0f;
float CollisionCheckDelay = 0.0f; float CollisionCheckDelay = 0.0f;
float Size = 0.0f; float Size = 0.0f;
int ClusterSize = 1;
bool Enabled = false; bool Enabled = false;
bool Stopped = false; bool Stopped = false;

View file

@ -1022,6 +1022,8 @@ namespace TEN::Renderer
void Renderer::PrepareWeatherParticles(RenderView& view) void Renderer::PrepareWeatherParticles(RenderView& view)
{ {
constexpr auto RAIN_WIDTH = 4.0f; constexpr auto RAIN_WIDTH = 4.0f;
constexpr auto SNOW_CLUSTER_SPREAD = BLOCK(1.0f);
constexpr auto RAIN_CLUSTER_SPREAD = BLOCK(0.35f);
for (const auto& part : Weather.GetParticles()) for (const auto& part : Weather.GetParticles())
{ {
@ -1031,15 +1033,14 @@ namespace TEN::Renderer
auto pos = Vector3::Lerp(part.PrevPosition, part.Position, GetInterpolationFactor()); auto pos = Vector3::Lerp(part.PrevPosition, part.Position, GetInterpolationFactor());
auto size = Lerp(part.PrevSize, part.Size, GetInterpolationFactor()); auto size = Lerp(part.PrevSize, part.Size, GetInterpolationFactor());
if (!view.Camera.Frustum.SphereInFrustum(pos, size)) // Underwater dust does not need clustering.
continue; if (part.Type == WeatherType::None)
switch (part.Type)
{ {
case WeatherType::None: if (!view.Camera.Frustum.SphereInFrustum(pos, size))
continue;
if (!CheckIfSlotExists(ID_DEFAULT_SPRITES, "Underwater dust rendering")) if (!CheckIfSlotExists(ID_DEFAULT_SPRITES, "Underwater dust rendering"))
return; continue;
AddSpriteBillboard( AddSpriteBillboard(
&_sprites[Objects[ID_DEFAULT_SPRITES].meshIndex + SPR_UNDERWATERDUST], &_sprites[Objects[ID_DEFAULT_SPRITES].meshIndex + SPR_UNDERWATERDUST],
@ -1048,39 +1049,103 @@ namespace TEN::Renderer
0.0f, 1.0f, Vector2(size), 0.0f, 1.0f, Vector2(size),
BlendMode::Additive, true, view); BlendMode::Additive, true, view);
break; continue;
}
case WeatherType::Snow: // Clamp cluster size to 1.
int clusterSize = std::max(1, part.ClusterSize);
if (!CheckIfSlotExists(ID_DEFAULT_SPRITES, "Snow rendering")) // If particle is dying, immediately cancel the cluster.
return; if (part.Stopped)
clusterSize = 1;
AddSpriteBillboard( auto finalPos = pos;
&_sprites[Objects[ID_DEFAULT_SPRITES].meshIndex + SPR_UNDERWATERDUST], auto finalScale = size;
pos,
Color(1.0f, 1.0f, 1.0f, part.Transparency()),
0.0f, 1.0f, Vector2(size),
BlendMode::Additive, true, view);
break; for (int i = 0; i < clusterSize; i++)
{
if (i > 0)
{
// Combine particle index and cluster index for unique seeding.
int uniqueSeed = part.UniqueID + i;
case WeatherType::Rain: // Use bits from uniqueSeed to determine distribution pattern.
float spread = part.Type == WeatherType::Snow ? SNOW_CLUSTER_SPREAD : RAIN_CLUSTER_SPREAD;
float offsetBase = spread * ((i + 1) / (float)clusterSize);
if (!CheckIfSlotExists(ID_DRIP_SPRITE, "Rain rendering")) // Use bits 0, 1, 2 for axis signs.
return; // Snow Y axis is always negative, so that snowflakes don't sink into room geometry.
float xSign = (uniqueSeed & 1) ? 1.0f : -1.0f;
float zSign = (uniqueSeed & 4) ? 1.0f : -1.0f;
Vector3 v; // Use bits 3, 4 for axis emphasis.
part.Velocity.Normalize(v); int axisEmphasis = uniqueSeed & 3;
float xScale = (axisEmphasis == 0) ? 1.1f : 0.4f;
float yScale = (axisEmphasis == 1) ? 1.2f : 0.5f;
float zScale = (axisEmphasis == 2) ? 1.0f : 0.6f;
AddSpriteBillboardConstrained( Vector3 positionOffset(
&_sprites[Objects[ID_DRIP_SPRITE].meshIndex], xSign * offsetBase * xScale,
pos, -(offsetBase * yScale),
Color(0.8f, 1.0f, 1.0f, part.Transparency()), zSign * offsetBase * zScale
0.0f, 1.0f, );
Vector2(RAIN_WIDTH, size),
BlendMode::Additive, -v, true, view);
break; // Apply deterministic offset.
finalPos = pos + positionOffset;
finalScale = size * (1.0f + abs(phd_sin(part.UniqueID + i)));
constexpr auto SNOW_SPIN_RATE = 0.05f;
constexpr auto SNOW_SPIN_RADIUS = 0.3f;
if (part.Type == WeatherType::Snow)
{
// Calculate spin angle based on vertical position.
// Wrap the vertical position to 3 blocks and multiply by 21 to get full unsigned short value.
unsigned short spinAngle = ((int)abs(finalPos.y) % BLOCK(3)) * 21;
// Apply circular motion in XZ plane.
finalPos.x += positionOffset.x * phd_sin((short)spinAngle);
finalPos.z += positionOffset.z * phd_cos((short)spinAngle);
}
}
if (!view.Camera.Frustum.SphereInFrustum(finalPos, finalScale))
continue;
switch (part.Type)
{
case WeatherType::Snow:
if (!CheckIfSlotExists(ID_DEFAULT_SPRITES, "Snow rendering"))
return;
AddSpriteBillboard(
&_sprites[Objects[ID_DEFAULT_SPRITES].meshIndex + SPR_UNDERWATERDUST],
finalPos,
Color(1.0f, 1.0f, 1.0f, part.Transparency()),
0.0f, 1.0f, Vector2(finalScale),
BlendMode::Additive, false, view);
break;
case WeatherType::Rain:
if (!CheckIfSlotExists(ID_DRIP_SPRITE, "Rain rendering"))
return;
Vector3 v;
part.Velocity.Normalize(v);
AddSpriteBillboardConstrained(
&_sprites[Objects[ID_DRIP_SPRITE].meshIndex],
finalPos,
Color(0.8f, 1.0f, 1.0f, part.Transparency()),
0.0f, 1.0f,
Vector2(RAIN_WIDTH, finalScale),
BlendMode::Additive, -v, false, view);
break;
}
} }
} }
} }