Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added realistic movement trail #5702

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions shaders/Ripple.gdshader
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
shader_type spatial;

uniform sampler2D bubble_noise;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

camelCase naming should be used in Thrive shaders.

uniform sampler2D bubble_gradient;
uniform float smoothness : hint_range(0.0, 1.0, 0.05) = 0.5;
uniform float refraction_strength : hint_range(0.0, 1.0, 0.05) = 0.2;
varying vec3 world_pos;

void vertex() {
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
float height = texture(bubble_noise, VERTEX.xz * smoothness + vec2(TIME * 0.1)).r;
VERTEX += NORMAL * height * 0.1;
}

float fresnel_effect(vec3 normal, vec3 view) {
float fresnel = dot(normalize(view), normalize(normal));
fresnel = clamp(abs(fresnel), 0.0, 1.0);
return pow(1.0 - fresnel, 2.0);
}

void fragment() {
float fresnel = fresnel_effect(NORMAL, VIEW);
vec2 distorted_uv = world_pos.xz + vec2(TIME * 0.2);
distorted_uv += NORMAL.xz * refraction_strength;
vec3 gradient_color = texture(bubble_gradient, distorted_uv).rgb;

ALBEDO = mix(vec3(1.0), gradient_color, 0.5);
METALLIC = 0.9;
ROUGHNESS = 0.01;
SPECULAR = 0.8;
ALPHA = fresnel * 0.5;
}
1 change: 1 addition & 0 deletions src/extension/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ add_library(thrive_extension SHARED
core/ThriveConfig.cpp core/ThriveConfig.hpp
interop/ExtensionInterop.cpp interop/ExtensionInterop.h
nodes/DebugDrawer.cpp nodes/DebugDrawer.hpp
nodes/Ripple.cpp nodes/Ripple.hpp
core/GodotJoltConversions.hpp
)

Expand Down
3 changes: 3 additions & 0 deletions src/extension/core/RegisterThriveTypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#include "nodes/DebugDrawer.hpp"

#include "nodes/Ripple.hpp"

#include "ThriveConfig.hpp"

// ------------------------------------ //
Expand All @@ -17,6 +19,7 @@ void InitializeThriveModule(godot::ModuleInitializationLevel level)

GDREGISTER_CLASS(Thrive::ThriveConfig);
GDREGISTER_CLASS(Thrive::DebugDrawer);
GDREGISTER_CLASS(Thrive::Ripple);
}

void UnInitializeThriveModule(godot::ModuleInitializationLevel level)
Expand Down
196 changes: 196 additions & 0 deletions src/extension/nodes/Ripple.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#include "Ripple.hpp"

namespace Thrive {

// Binds methods to be accessible from Godot
void Ripple::_bind_methods() {
using namespace godot;

// Method binder
ClassDB::bind_method(D_METHOD("set_material", "material"), &Ripple::SetMaterial);
ClassDB::bind_method(D_METHOD("get_material"), &Ripple::GetMaterial);

// Add the material property to make it visible and editable in Godot editor
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "material",
PROPERTY_HINT_RESOURCE_TYPE, "Material",
PROPERTY_USAGE_DEFAULT),
"set_material", "get_material");
}

// Constructor: Initializes the Ripple effect and enables processing
Ripple::Ripple() {
// Enable _process to be called every frame
set_process(true);
}

// Destructor: Cleans up resources when the Ripple effect is destroyed
Ripple::~Ripple() {
isExiting = true;
if (meshInstance) {
meshInstance->queue_free();
}
}

// Called when the node exits the scene tree
void Ripple::_exit_tree() {
// clean up references when leaving the scene
isExiting = true;
if (sharedParticles) {
sharedParticles = nullptr; // don't own this just clear reference
}
}

// Called when the node enters the scene tree
void Ripple::_ready() {
// create and set up the immediate mesh for dynamic geometry
mesh.instantiate();
meshInstance = memnew(godot::MeshInstance3D);
add_child(meshInstance);
meshInstance->set_mesh(mesh);

// handle material setup
if (material.is_valid()) {
// use existing material if one was set
meshInstance->set_material_override(material);
} else {
// create default transparent material if none provided
godot::Ref<godot::StandardMaterial3D> mat;
mat.instantiate();
mat->set_transparency(godot::BaseMaterial3D::TRANSPARENCY_ALPHA);
mat->set_shading_mode(godot::BaseMaterial3D::SHADING_MODE_UNSHADED);
mat->set_albedo(godot::Color(1, 0, 0, 0.5));
SetMaterial(mat);
}

// get reference to the shared particle system in the scene
sharedParticles = get_node<godot::CPUParticles3D>("SharedParticles");

// store initial position for movement tracking
lastPosition = get_global_position();
initialized = true;
}

// Called every frame to update the ripple effect
void Ripple::_process(double delta) {
// skip processing if not properly initialized or being destroyed
if (!initialized || !meshInstance || isExiting)
return;

// update ripple points, mesh geometry and particle emission
ManageRipplePoints(static_cast<float>(delta));
UpdateMeshGeometry();
UpdateParticleEmission();
}

// Manages the array of points that form the ripple trail
void Ripple::ManageRipplePoints(float delta) {
// get current position in world space
godot::Vector3 currentPosition = get_global_position();

// only create new points when movement threshold is exceeded
if ((currentPosition - lastPosition).length() > MIN_POINT_DISTANCE) {
// shift existing points if at capacity
if (numPoints >= MAX_POINTS) {
// move all points one position back discarding the oldest
for (int i = 0; i < MAX_POINTS - 1; i++) {
points[i] = points[i + 1];
}
numPoints = MAX_POINTS - 1;
}

// add new point at current position
points[numPoints].position = currentPosition;
points[numPoints].age = 0;
numPoints++;

// update last position for next frame's comparison
lastPosition = currentPosition;
}

// update ages and remove expired points
int alivePoints = 0;
for (int i = 0; i < numPoints; i++) {
// increment age of point
points[i].age += delta;

// keep point if still within lifetime
if (points[i].age < lifetime) {
// compact array by moving valid points to front if needed
if (i != alivePoints) {
points[alivePoints] = points[i];
}
alivePoints++;
}
}
// update count of valid points
numPoints = alivePoints;
}

// Updates the mesh geometry based on current ripple points
void Ripple::UpdateMeshGeometry() {
// clear previous geometry
mesh->clear_surfaces();

// need at least 2 points to create geometry
if (numPoints < 2)
return;

// begin creating triangle strip for the ripple trail
mesh->surface_begin(godot::Mesh::PRIMITIVE_TRIANGLE_STRIP);

for (int i = 0; i < numPoints; i++) {
// calculate fade based on point age
float alpha = 1.0f - (points[i].age / lifetime);
godot::Color color(1, 0, 0, alpha);

// calculate direction vector between points
godot::Vector3 direction;
if (i < numPoints - 1) {
// use direction to next point for all except last point
direction = (points[i + 1].position - points[i].position).normalized();
} else {
// use direction from previous point for last point
direction = (points[i].position - points[i - 1].position).normalized();
}

// cross product with up vector creates perpendicular vector
godot::Vector3 side = direction.cross(godot::Vector3(0, 1, 0)).normalized() * rippleWidth;

// add vertices for both sides of the trail
mesh->surface_set_color(color);
mesh->surface_add_vertex(to_local(points[i].position + side));
mesh->surface_set_color(color);
mesh->surface_add_vertex(to_local(points[i].position - side));
}

// Finish the mesh surface
mesh->surface_end();
}

// Updates the shared particle system's emission position
void Ripple::UpdateParticleEmission() {
// only update particles if we have a valid particle system and have moved enough
if (sharedParticles && (lastPosition - get_global_position()).length() > MIN_POINT_DISTANCE) {
// enable emission and update particle system position
sharedParticles->set_emitting(true);
sharedParticles->set_global_position(get_global_position());
}
}

// Sets the material used for rendering the ripple effect
void Ripple::SetMaterial(const godot::Ref<godot::Material>& p_material) {
// store the material reference
material = p_material;

// apply material immediately if mesh instance exists
if (meshInstance) {
meshInstance->set_material_override(material);
}
}

// Returns the current material used by the ripple effect
godot::Ref<godot::Material> Ripple::GetMaterial() const {
return material;
}

} // namespace Thrive
111 changes: 111 additions & 0 deletions src/extension/nodes/Ripple.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#pragma once

#include <godot_cpp/classes/mesh.hpp>
#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/classes/mesh_instance3d.hpp>
#include <godot_cpp/classes/material.hpp>
#include <godot_cpp/classes/cpu_particles3d.hpp>
#include <godot_cpp/classes/immediate_mesh.hpp>
#include <godot_cpp/classes/standard_material3d.hpp>
#include <godot_cpp/core/class_db.hpp>

namespace Thrive {

/**
* The Ripple class creates a visual trails that follow moving microbes,
* gradually fading away like the name suggest, ripples in water.
*/
class Ripple : public godot::Node3D {
GDCLASS(Ripple, godot::Node3D)

private:
/**
* Holds the data for a single point in our ripple trail
*/
struct RipplePoint {
godot::Vector3 position; ///< where does this point sit in the world?
float age; ///< how long this point has existed or used for fading?
};

/// Keep track of this many points
static constexpr size_t MAX_POINTS = 20;

/// All our ripple points live here
RipplePoint points[MAX_POINTS];

/// How many points are currently active
int numPoints = 0;

/// How wide our ripple effect should be
float rippleWidth = 0.12f;

/// How long each ripple point sticks around before fading away
float lifetime = 0.3f;

/// Don't create new points unless we've moved at least this far
static constexpr float MIN_POINT_DISTANCE = 0.01f;

/// The 3D object that shows our ripple effect
godot::MeshInstance3D* meshInstance = nullptr;

/// The mesh we update dynamically as ripples form
godot::Ref<godot::ImmediateMesh> mesh;

/// The material that controls how our ripples look
godot::Ref<godot::Material> material;

/// Remember where we were last frame to track movement
godot::Vector3 lastPosition;

/// Connection to the particle system we share with other effects
godot::CPUParticles3D* sharedParticles = nullptr;

/// Keep track of our setup state
bool initialized = false;
bool isExiting = false;

protected:
static void _bind_methods();

public:
Ripple();
~Ripple();

void _ready() override;
void _process(double delta) override;
void _exit_tree() override;

/**
* Changes the material used for our ripple effect
* @param p_material The new material to use
*/
void SetMaterial(const godot::Ref<godot::Material>& p_material);

/**
* Gets the material we're currently using for the ripple effect
* @return The current material
*/
godot::Ref<godot::Material> GetMaterial() const;

private:
/**
* Updates our mesh to match the current ripple points.
* Creates a smooth trail using triangle strips.
*/
void UpdateMeshGeometry();

/**
* Keeps the particle system in sync with our movement.
* Makes sure particles appear in the right place.
*/
void UpdateParticleEmission();

/**
* Handles the lifecycle of our ripple points - adds new ones
* as we move and removes old ones as they fade out
* @param delta Time since last frame
*/
void ManageRipplePoints(float delta);
};

} // namespace Thrive
Loading