-
-
Notifications
You must be signed in to change notification settings - Fork 508
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
AverageNerdz
wants to merge
5
commits into
master
Choose a base branch
from
bubble_trail
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3ef1aba
Update RegisterThriveTypes.cpp to accomodate extension include and such
AverageNerdz 28f447b
Update CMakeLists.txt to accomodate the extension
AverageNerdz 0910aed
Added core files for the extension
AverageNerdz 3575926
Added bubble ripple shader
AverageNerdz e9255c5
Edited to handle 3D trail and ripple emitter
AverageNerdz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
shader_type spatial; | ||
|
||
uniform sampler2D bubble_noise; | ||
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.