The hardcoded age limit is now gone. The behavior can be implemented with an Age Reached Event and Kill Particle node. Other utility nodes to handle age limits of particles can be added later. Adding an Age Limit attribute to particles on birth will be useful for some effects, e.g. when you want to control the color or size of a particle over its life time. The Random Float node takes a seed currently. Different nodes will produce different values even with the same seed. However, the same node will generate the same random number for the same seed every time. The "Hash" of a particle can be used as seed. Later, we'd want to have more modes in the node to make it more user friendly. Modes could be: Per Particle, Per Time, Per Particle Per Time, Per Node Instance, ... Also a Random Vector node will be useful, as it currently has to be build using three Random Float nodes.
523 lines
20 KiB
C++
523 lines
20 KiB
C++
/*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "simulation_solver.hh"
|
|
|
|
#include "BKE_customdata.h"
|
|
#include "BKE_persistent_data_handle.hh"
|
|
|
|
#include "BLI_rand.hh"
|
|
#include "BLI_set.hh"
|
|
|
|
#include "DEG_depsgraph_query.h"
|
|
|
|
namespace blender::sim {
|
|
|
|
static CustomDataType cpp_to_custom_data_type(const CPPType &type)
|
|
{
|
|
if (type.is<float3>()) {
|
|
return CD_PROP_FLOAT3;
|
|
}
|
|
if (type.is<float>()) {
|
|
return CD_PROP_FLOAT;
|
|
}
|
|
if (type.is<int32_t>()) {
|
|
return CD_PROP_INT32;
|
|
}
|
|
BLI_assert(false);
|
|
return CD_PROP_FLOAT;
|
|
}
|
|
|
|
static const CPPType &custom_to_cpp_data_type(CustomDataType type)
|
|
{
|
|
switch (type) {
|
|
case CD_PROP_FLOAT3:
|
|
return CPPType::get<float3>();
|
|
case CD_PROP_FLOAT:
|
|
return CPPType::get<float>();
|
|
case CD_PROP_INT32:
|
|
return CPPType::get<int32_t>();
|
|
default:
|
|
BLI_assert(false);
|
|
return CPPType::get<float>();
|
|
}
|
|
}
|
|
|
|
class CustomDataAttributesRef {
|
|
private:
|
|
Array<void *> buffers_;
|
|
int64_t size_;
|
|
const AttributesInfo &info_;
|
|
|
|
public:
|
|
CustomDataAttributesRef(CustomData &custom_data, int64_t size, const AttributesInfo &info)
|
|
: buffers_(info.size(), nullptr), size_(size), info_(info)
|
|
{
|
|
for (int attribute_index : info.index_range()) {
|
|
StringRefNull name = info.name_of(attribute_index);
|
|
const CPPType &cpp_type = info.type_of(attribute_index);
|
|
CustomDataType custom_type = cpp_to_custom_data_type(cpp_type);
|
|
void *data = CustomData_get_layer_named(&custom_data, custom_type, name.c_str());
|
|
buffers_[attribute_index] = data;
|
|
}
|
|
}
|
|
|
|
operator MutableAttributesRef()
|
|
{
|
|
return MutableAttributesRef(info_, buffers_, size_);
|
|
}
|
|
|
|
operator AttributesRef() const
|
|
{
|
|
return AttributesRef(info_, buffers_, size_);
|
|
}
|
|
};
|
|
|
|
static void ensure_attributes_exist(ParticleSimulationState *state, const AttributesInfo &info)
|
|
{
|
|
bool found_layer_to_remove;
|
|
do {
|
|
found_layer_to_remove = false;
|
|
for (int layer_index = 0; layer_index < state->attributes.totlayer; layer_index++) {
|
|
CustomDataLayer *layer = &state->attributes.layers[layer_index];
|
|
BLI_assert(layer->name != nullptr);
|
|
const CPPType &cpp_type = custom_to_cpp_data_type((CustomDataType)layer->type);
|
|
StringRefNull name = layer->name;
|
|
if (!info.has_attribute(name, cpp_type)) {
|
|
found_layer_to_remove = true;
|
|
CustomData_free_layer(&state->attributes, layer->type, state->tot_particles, layer_index);
|
|
break;
|
|
}
|
|
}
|
|
} while (found_layer_to_remove);
|
|
|
|
for (int attribute_index : info.index_range()) {
|
|
StringRefNull attribute_name = info.name_of(attribute_index);
|
|
const CPPType &cpp_type = info.type_of(attribute_index);
|
|
CustomDataType custom_type = cpp_to_custom_data_type(cpp_type);
|
|
if (CustomData_get_layer_named(&state->attributes, custom_type, attribute_name.c_str()) ==
|
|
nullptr) {
|
|
void *data = CustomData_add_layer_named(&state->attributes,
|
|
custom_type,
|
|
CD_CALLOC,
|
|
nullptr,
|
|
state->tot_particles,
|
|
attribute_name.c_str());
|
|
cpp_type.fill_uninitialized(info.default_of(attribute_index), data, state->tot_particles);
|
|
}
|
|
}
|
|
}
|
|
|
|
BLI_NOINLINE static void apply_remaining_diffs(ParticleChunkContext &context)
|
|
{
|
|
BLI_assert(context.integration != nullptr);
|
|
MutableSpan<float3> positions = context.attributes.get<float3>("Position");
|
|
MutableSpan<float3> velocities = context.attributes.get<float3>("Velocity");
|
|
|
|
for (int i : context.index_mask) {
|
|
positions[i] += context.integration->position_diffs[i];
|
|
velocities[i] += context.integration->velocity_diffs[i];
|
|
}
|
|
}
|
|
|
|
BLI_NOINLINE static void find_next_event_per_particle(
|
|
SimulationSolveContext &solve_context,
|
|
ParticleChunkContext &particles,
|
|
Span<const ParticleEvent *> events,
|
|
MutableSpan<int> r_next_event_indices,
|
|
MutableSpan<float> r_time_factors_to_next_event)
|
|
{
|
|
r_next_event_indices.fill_indices(particles.index_mask, -1);
|
|
r_time_factors_to_next_event.fill_indices(particles.index_mask, 1.0f);
|
|
|
|
Array<float> time_factors(particles.index_mask.min_array_size());
|
|
for (int event_index : events.index_range()) {
|
|
time_factors.fill(-1.0f);
|
|
ParticleEventFilterContext event_context{solve_context, particles, time_factors};
|
|
const ParticleEvent &event = *events[event_index];
|
|
event.filter(event_context);
|
|
|
|
for (int i : particles.index_mask) {
|
|
const float time_factor = time_factors[i];
|
|
const float previously_smallest_time_factor = r_time_factors_to_next_event[i];
|
|
if (time_factor >= 0.0f && time_factor <= previously_smallest_time_factor) {
|
|
r_time_factors_to_next_event[i] = time_factor;
|
|
r_next_event_indices[i] = event_index;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
BLI_NOINLINE static void forward_particles_to_next_event_or_end(
|
|
ParticleChunkContext &particles, Span<float> time_factors_to_next_event)
|
|
{
|
|
MutableSpan<float3> positions = particles.attributes.get<float3>("Position");
|
|
MutableSpan<float3> velocities = particles.attributes.get<float3>("Velocity");
|
|
|
|
MutableSpan<float3> position_diffs = particles.integration->position_diffs;
|
|
MutableSpan<float3> velocity_diffs = particles.integration->velocity_diffs;
|
|
MutableSpan<float> durations = particles.integration->durations;
|
|
|
|
for (int i : particles.index_mask) {
|
|
const float time_factor = time_factors_to_next_event[i];
|
|
positions[i] += position_diffs[i] * time_factor;
|
|
velocities[i] += velocity_diffs[i] * time_factor;
|
|
|
|
const float remaining_time_factor = 1.0f - time_factor;
|
|
position_diffs[i] *= remaining_time_factor;
|
|
velocity_diffs[i] *= remaining_time_factor;
|
|
durations[i] *= remaining_time_factor;
|
|
}
|
|
}
|
|
|
|
BLI_NOINLINE static void group_particles_by_event(
|
|
IndexMask mask,
|
|
Span<int> next_event_indices,
|
|
MutableSpan<Vector<int64_t>> r_particles_per_event)
|
|
{
|
|
for (int i : mask) {
|
|
int event_index = next_event_indices[i];
|
|
if (event_index >= 0) {
|
|
r_particles_per_event[event_index].append(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
BLI_NOINLINE static void execute_events(SimulationSolveContext &solve_context,
|
|
ParticleChunkContext &all_particles,
|
|
Span<const ParticleEvent *> events,
|
|
Span<Vector<int64_t>> particles_per_event)
|
|
{
|
|
for (int event_index : events.index_range()) {
|
|
Span<int64_t> pindices = particles_per_event[event_index];
|
|
if (pindices.is_empty()) {
|
|
continue;
|
|
}
|
|
|
|
const ParticleEvent &event = *events[event_index];
|
|
ParticleChunkContext particles{
|
|
all_particles.state, pindices, all_particles.attributes, all_particles.integration};
|
|
ParticleActionContext action_context{solve_context, particles};
|
|
event.execute(action_context);
|
|
}
|
|
}
|
|
|
|
BLI_NOINLINE static void find_unfinished_particles(IndexMask index_mask,
|
|
Span<float> time_factors_to_next_event,
|
|
Vector<int64_t> &r_unfinished_pindices)
|
|
{
|
|
for (int i : index_mask) {
|
|
float time_factor = time_factors_to_next_event[i];
|
|
if (time_factor < 1.0f) {
|
|
r_unfinished_pindices.append(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
BLI_NOINLINE static void simulate_to_next_event(SimulationSolveContext &solve_context,
|
|
ParticleChunkContext &particles,
|
|
Span<const ParticleEvent *> events,
|
|
Vector<int64_t> &r_unfinished_pindices)
|
|
{
|
|
int array_size = particles.index_mask.min_array_size();
|
|
Array<int> next_event_indices(array_size);
|
|
Array<float> time_factors_to_next_event(array_size);
|
|
|
|
find_next_event_per_particle(
|
|
solve_context, particles, events, next_event_indices, time_factors_to_next_event);
|
|
|
|
forward_particles_to_next_event_or_end(particles, time_factors_to_next_event);
|
|
|
|
Array<Vector<int64_t>> particles_per_event(events.size());
|
|
group_particles_by_event(particles.index_mask, next_event_indices, particles_per_event);
|
|
|
|
execute_events(solve_context, particles, events, particles_per_event);
|
|
find_unfinished_particles(
|
|
particles.index_mask, time_factors_to_next_event, r_unfinished_pindices);
|
|
}
|
|
|
|
BLI_NOINLINE static void simulate_with_max_n_events(SimulationSolveContext &solve_context,
|
|
ParticleSimulationState &state,
|
|
ParticleChunkContext &particles,
|
|
int max_events)
|
|
{
|
|
Span<const ParticleEvent *> events = solve_context.influences.particle_events.lookup_as(
|
|
state.head.name);
|
|
if (events.size() == 0) {
|
|
apply_remaining_diffs(particles);
|
|
return;
|
|
}
|
|
|
|
Vector<int64_t> unfininished_pindices = particles.index_mask.indices();
|
|
for (int iteration : IndexRange(max_events)) {
|
|
UNUSED_VARS(iteration);
|
|
if (unfininished_pindices.is_empty()) {
|
|
break;
|
|
}
|
|
|
|
Vector<int64_t> new_unfinished_pindices;
|
|
ParticleChunkContext remaining_particles{particles.state,
|
|
unfininished_pindices.as_span(),
|
|
particles.attributes,
|
|
particles.integration};
|
|
simulate_to_next_event(solve_context, remaining_particles, events, new_unfinished_pindices);
|
|
unfininished_pindices = std::move(new_unfinished_pindices);
|
|
}
|
|
|
|
if (!unfininished_pindices.is_empty()) {
|
|
ParticleChunkContext remaining_particles{particles.state,
|
|
unfininished_pindices.as_span(),
|
|
particles.attributes,
|
|
particles.integration};
|
|
apply_remaining_diffs(remaining_particles);
|
|
}
|
|
}
|
|
|
|
BLI_NOINLINE static void simulate_particle_chunk(SimulationSolveContext &solve_context,
|
|
ParticleSimulationState &state,
|
|
MutableAttributesRef attributes,
|
|
MutableSpan<float> remaining_durations,
|
|
float end_time)
|
|
{
|
|
int particle_amount = attributes.size();
|
|
|
|
Span<const ParticleAction *> begin_actions =
|
|
solve_context.influences.particle_time_step_begin_actions.lookup_as(state.head.name);
|
|
for (const ParticleAction *action : begin_actions) {
|
|
ParticleChunkContext particles{state, IndexMask(particle_amount), attributes};
|
|
ParticleActionContext action_context{solve_context, particles};
|
|
action->execute(action_context);
|
|
}
|
|
|
|
Array<float3> force_vectors{particle_amount, {0, 0, 0}};
|
|
Span<const ParticleForce *> forces = solve_context.influences.particle_forces.lookup_as(
|
|
state.head.name);
|
|
for (const ParticleForce *force : forces) {
|
|
ParticleChunkContext particles{state, IndexMask(particle_amount), attributes};
|
|
ParticleForceContext particle_force_context{solve_context, particles, force_vectors};
|
|
force->add_force(particle_force_context);
|
|
}
|
|
|
|
MutableSpan<float3> velocities = attributes.get<float3>("Velocity");
|
|
|
|
Array<float3> position_diffs(particle_amount);
|
|
Array<float3> velocity_diffs(particle_amount);
|
|
for (int i : IndexRange(particle_amount)) {
|
|
const float time_step = remaining_durations[i];
|
|
velocity_diffs[i] = force_vectors[i] * time_step;
|
|
position_diffs[i] = (velocities[i] + velocity_diffs[i] / 2.0f) * time_step;
|
|
}
|
|
|
|
ParticleChunkIntegrationContext integration_context = {
|
|
position_diffs, velocity_diffs, remaining_durations, end_time};
|
|
ParticleChunkContext particle_chunk_context{
|
|
state, IndexMask(particle_amount), attributes, &integration_context};
|
|
|
|
simulate_with_max_n_events(solve_context, state, particle_chunk_context, 10);
|
|
|
|
Span<const ParticleAction *> end_actions =
|
|
solve_context.influences.particle_time_step_end_actions.lookup_as(state.head.name);
|
|
for (const ParticleAction *action : end_actions) {
|
|
ParticleChunkContext particles{state, IndexMask(particle_amount), attributes};
|
|
ParticleActionContext action_context{solve_context, particles};
|
|
action->execute(action_context);
|
|
}
|
|
}
|
|
|
|
BLI_NOINLINE static void simulate_existing_particles(SimulationSolveContext &solve_context,
|
|
ParticleSimulationState &state,
|
|
const AttributesInfo &attributes_info)
|
|
{
|
|
CustomDataAttributesRef custom_data_attributes{
|
|
state.attributes, state.tot_particles, attributes_info};
|
|
MutableAttributesRef attributes = custom_data_attributes;
|
|
|
|
Array<float> remaining_durations(state.tot_particles, solve_context.solve_interval.duration());
|
|
simulate_particle_chunk(
|
|
solve_context, state, attributes, remaining_durations, solve_context.solve_interval.stop());
|
|
}
|
|
|
|
BLI_NOINLINE static void run_emitters(SimulationSolveContext &solve_context,
|
|
ParticleAllocators &particle_allocators)
|
|
{
|
|
for (const ParticleEmitter *emitter : solve_context.influences.particle_emitters) {
|
|
ParticleEmitterContext emitter_context{
|
|
solve_context, particle_allocators, solve_context.solve_interval};
|
|
emitter->emit(emitter_context);
|
|
}
|
|
}
|
|
|
|
BLI_NOINLINE static int count_particles_after_time_step(ParticleSimulationState &state,
|
|
ParticleAllocator &allocator)
|
|
{
|
|
CustomDataAttributesRef custom_data_attributes{
|
|
state.attributes, state.tot_particles, allocator.attributes_info()};
|
|
MutableAttributesRef attributes = custom_data_attributes;
|
|
int new_particle_amount = attributes.get<int>("Dead").count(0);
|
|
|
|
for (MutableAttributesRef emitted_attributes : allocator.get_allocations()) {
|
|
new_particle_amount += emitted_attributes.get<int>("Dead").count(0);
|
|
}
|
|
|
|
return new_particle_amount;
|
|
}
|
|
|
|
BLI_NOINLINE static void remove_dead_and_add_new_particles(ParticleSimulationState &state,
|
|
ParticleAllocator &allocator)
|
|
{
|
|
const int new_particle_amount = count_particles_after_time_step(state, allocator);
|
|
|
|
CustomDataAttributesRef custom_data_attributes{
|
|
state.attributes, state.tot_particles, allocator.attributes_info()};
|
|
|
|
Vector<MutableAttributesRef> particle_sources;
|
|
particle_sources.append(custom_data_attributes);
|
|
particle_sources.extend(allocator.get_allocations());
|
|
|
|
CustomDataLayer *dead_layer = nullptr;
|
|
|
|
for (CustomDataLayer &layer : MutableSpan(state.attributes.layers, state.attributes.totlayer)) {
|
|
StringRefNull name = layer.name;
|
|
if (name == "Dead") {
|
|
dead_layer = &layer;
|
|
continue;
|
|
}
|
|
const CPPType &cpp_type = custom_to_cpp_data_type((CustomDataType)layer.type);
|
|
GMutableSpan new_buffer{
|
|
cpp_type,
|
|
MEM_mallocN_aligned(new_particle_amount * cpp_type.size(), cpp_type.alignment(), AT),
|
|
new_particle_amount};
|
|
|
|
int current = 0;
|
|
for (MutableAttributesRef attributes : particle_sources) {
|
|
Span<int> dead_states = attributes.get<int>("Dead");
|
|
GSpan source_buffer = attributes.get(name);
|
|
BLI_assert(source_buffer.type() == cpp_type);
|
|
for (int i : attributes.index_range()) {
|
|
if (dead_states[i] == 0) {
|
|
cpp_type.copy_to_uninitialized(source_buffer[i], new_buffer[current]);
|
|
current++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (layer.data != nullptr) {
|
|
MEM_freeN(layer.data);
|
|
}
|
|
layer.data = new_buffer.data();
|
|
}
|
|
|
|
BLI_assert(dead_layer != nullptr);
|
|
if (dead_layer->data != nullptr) {
|
|
MEM_freeN(dead_layer->data);
|
|
}
|
|
dead_layer->data = MEM_callocN(sizeof(int) * new_particle_amount, AT);
|
|
|
|
state.tot_particles = new_particle_amount;
|
|
state.next_particle_id += allocator.total_allocated();
|
|
}
|
|
|
|
void initialize_simulation_states(Simulation &simulation,
|
|
Depsgraph &UNUSED(depsgraph),
|
|
const SimulationInfluences &UNUSED(influences),
|
|
const bke::PersistentDataHandleMap &UNUSED(handle_map))
|
|
{
|
|
simulation.current_simulation_time = 0.0f;
|
|
}
|
|
|
|
void solve_simulation_time_step(Simulation &simulation,
|
|
Depsgraph &depsgraph,
|
|
const SimulationInfluences &influences,
|
|
const bke::PersistentDataHandleMap &handle_map,
|
|
const DependencyAnimations &dependency_animations,
|
|
float time_step)
|
|
{
|
|
SimulationStateMap state_map;
|
|
LISTBASE_FOREACH (SimulationState *, state, &simulation.states) {
|
|
state_map.add(state);
|
|
}
|
|
|
|
SimulationSolveContext solve_context{simulation,
|
|
depsgraph,
|
|
influences,
|
|
TimeInterval(simulation.current_simulation_time, time_step),
|
|
state_map,
|
|
handle_map,
|
|
dependency_animations};
|
|
|
|
Span<ParticleSimulationState *> particle_simulation_states =
|
|
state_map.lookup<ParticleSimulationState>();
|
|
|
|
Map<std::string, std::unique_ptr<AttributesInfo>> attribute_infos;
|
|
Map<std::string, std::unique_ptr<ParticleAllocator>> particle_allocators_map;
|
|
for (ParticleSimulationState *state : particle_simulation_states) {
|
|
const AttributesInfoBuilder &builder = *influences.particle_attributes_builder.lookup_as(
|
|
state->head.name);
|
|
auto info = std::make_unique<AttributesInfo>(builder);
|
|
|
|
ensure_attributes_exist(state, *info);
|
|
|
|
uint32_t hash_seed = DefaultHash<StringRef>{}(state->head.name);
|
|
particle_allocators_map.add_new(
|
|
state->head.name,
|
|
std::make_unique<ParticleAllocator>(*info, state->next_particle_id, hash_seed));
|
|
attribute_infos.add_new(state->head.name, std::move(info));
|
|
}
|
|
|
|
ParticleAllocators particle_allocators{particle_allocators_map};
|
|
|
|
for (ParticleSimulationState *state : particle_simulation_states) {
|
|
const AttributesInfo &attributes_info = *attribute_infos.lookup_as(state->head.name);
|
|
simulate_existing_particles(solve_context, *state, attributes_info);
|
|
}
|
|
|
|
run_emitters(solve_context, particle_allocators);
|
|
|
|
for (ParticleSimulationState *state : particle_simulation_states) {
|
|
ParticleAllocator &allocator = *particle_allocators.try_get_allocator(state->head.name);
|
|
|
|
for (MutableAttributesRef attributes : allocator.get_allocations()) {
|
|
Span<const ParticleAction *> actions = influences.particle_birth_actions.lookup_as(
|
|
state->head.name);
|
|
for (const ParticleAction *action : actions) {
|
|
ParticleChunkContext chunk_context{*state, IndexRange(attributes.size()), attributes};
|
|
ParticleActionContext action_context{solve_context, chunk_context};
|
|
action->execute(action_context);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (ParticleSimulationState *state : particle_simulation_states) {
|
|
ParticleAllocator &allocator = *particle_allocators.try_get_allocator(state->head.name);
|
|
|
|
for (MutableAttributesRef attributes : allocator.get_allocations()) {
|
|
Array<float> remaining_durations(attributes.size());
|
|
Span<float> birth_times = attributes.get<float>("Birth Time");
|
|
const float end_time = solve_context.solve_interval.stop();
|
|
for (int i : attributes.index_range()) {
|
|
remaining_durations[i] = end_time - birth_times[i];
|
|
}
|
|
simulate_particle_chunk(solve_context, *state, attributes, remaining_durations, end_time);
|
|
}
|
|
|
|
remove_dead_and_add_new_particles(*state, allocator);
|
|
}
|
|
|
|
simulation.current_simulation_time = solve_context.solve_interval.stop();
|
|
}
|
|
|
|
} // namespace blender::sim
|