diff --git a/framework/opengl_renderer/CMakeLists.txt b/framework/opengl_renderer/CMakeLists.txt index d1b683c..f9a2eb3 100644 --- a/framework/opengl_renderer/CMakeLists.txt +++ b/framework/opengl_renderer/CMakeLists.txt @@ -268,6 +268,27 @@ target_link_libraries(bloom bloom_combine_render_task ) +############# lite_particles2d ########### + +add_library(lite_particles2d + src/mm/opengl/lite_particles2d_type.hpp + src/mm/opengl/lite_particles2d_type_loader.hpp + src/mm/opengl/lite_particles2d_type_loader.cpp + + src/mm/opengl/components/lite_particles2d.hpp + + src/mm/opengl/render_tasks/lite_particles2d.hpp + src/mm/opengl/render_tasks/lite_particles2d.cpp +) + +target_include_directories(lite_particles2d PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") + +target_link_libraries(lite_particles2d + engine + opengl_renderer_s + common_components_serialize_json # glm serl +) + ######################## if (BUILD_TESTING) diff --git a/framework/opengl_renderer/src/mm/opengl/components/lite_particles2d.hpp b/framework/opengl_renderer/src/mm/opengl/components/lite_particles2d.hpp new file mode 100644 index 0000000..354ac8f --- /dev/null +++ b/framework/opengl_renderer/src/mm/opengl/components/lite_particles2d.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include // tmp + +namespace MM::Components { + + // not intended to be a component + // see LiteParticles2DUploadQueue + struct LiteParticle2D { + uint32_t type_id {0u}; // entt::hashed_string, ResourceManager + + glm::vec2 pos {0.f, 0.f}; + glm::vec2 vel {0.f, 0.f}; + + float age {0.f}; + }; + + struct LiteParticles2DUploadQueue { + // TODO: vector + std::queue queue; + }; + +} // MM::Components + diff --git a/framework/opengl_renderer/src/mm/opengl/lite_particles2d_type.hpp b/framework/opengl_renderer/src/mm/opengl/lite_particles2d_type.hpp new file mode 100644 index 0000000..5bd6a92 --- /dev/null +++ b/framework/opengl_renderer/src/mm/opengl/lite_particles2d_type.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include + +#include + +#include +#include + +namespace MM::OpenGL { + + struct LiteParticles2DType { + struct Compute { + float age_delta; + + glm::vec2 force_vec; + + float turbulence; + float turbulence_noise_scale; + float turbulence_individuality; + float turbulence_time_scale; + + float dampening; + + static constexpr size_t _member_count = 8; + } compute; + + struct Render { + glm::vec4 color_start; + glm::vec4 color_end; + + float size_start; + float size_end; + + static constexpr size_t _member_count = 10; + } render; + + // naive, prob fine, not time critical + void upload(std::vector& comp_vec, std::vector& rend_vec) const { + { + comp_vec.push_back(compute.age_delta); + + comp_vec.push_back(compute.force_vec.x); + comp_vec.push_back(compute.force_vec.y); + + comp_vec.push_back(compute.turbulence); + comp_vec.push_back(compute.turbulence_noise_scale); + comp_vec.push_back(compute.turbulence_individuality); + comp_vec.push_back(compute.turbulence_time_scale); + + comp_vec.push_back(compute.dampening); + } + + { + rend_vec.push_back(render.color_start.r); + rend_vec.push_back(render.color_start.g); + rend_vec.push_back(render.color_start.b); + rend_vec.push_back(render.color_start.a); + + rend_vec.push_back(render.color_end.r); + rend_vec.push_back(render.color_end.g); + rend_vec.push_back(render.color_end.b); + rend_vec.push_back(render.color_end.a); + + rend_vec.push_back(render.size_start); + rend_vec.push_back(render.size_end); + } + } + }; + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LiteParticles2DType::Compute, + age_delta, + + force_vec, + + turbulence, + turbulence_noise_scale, + turbulence_individuality, + turbulence_time_scale, + + dampening + ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LiteParticles2DType::Render, + color_start, + color_end, + + size_start, + size_end + ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LiteParticles2DType, compute, render) + +} // MM::OpenGL + diff --git a/framework/opengl_renderer/src/mm/opengl/lite_particles2d_type_loader.cpp b/framework/opengl_renderer/src/mm/opengl/lite_particles2d_type_loader.cpp new file mode 100644 index 0000000..7d21242 --- /dev/null +++ b/framework/opengl_renderer/src/mm/opengl/lite_particles2d_type_loader.cpp @@ -0,0 +1,39 @@ +#include "./lite_particles2d_type_loader.hpp" + +#include + +#include + +namespace MM::OpenGL { + +std::shared_ptr LiteParticles2DTypeLoaderJson::load(const nlohmann::json& j) const { + auto new_type = std::make_shared(); + + try { + *new_type = j; + } catch (...) { + SPDLOG_ERROR("failed parsing particle type json:\n{}", j.dump(2)); + return nullptr; + } + + return new_type; +} + +std::shared_ptr LiteParticles2DTypeLoaderFile::load(MM::Engine& engine, const std::string& path) const { + auto* fs = engine.tryService(); + if (fs == nullptr) { + // TODO: error + return nullptr; + } + + auto new_particle = LiteParticles2DTypeLoaderJson{}.load(fs->readJson(path.c_str())); + + if (!new_particle) { + SPDLOG_ERROR("particle type file: '{}'", path); + } + + return new_particle; +} + +} // MM::OpenGL + diff --git a/framework/opengl_renderer/src/mm/opengl/lite_particles2d_type_loader.hpp b/framework/opengl_renderer/src/mm/opengl/lite_particles2d_type_loader.hpp new file mode 100644 index 0000000..3f977b4 --- /dev/null +++ b/framework/opengl_renderer/src/mm/opengl/lite_particles2d_type_loader.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "./lite_particles2d_type.hpp" + +#include + +namespace MM::OpenGL { + +struct LiteParticles2DTypeLoaderJson final { + std::shared_ptr load(const nlohmann::json& j) const; +}; + +struct LiteParticles2DTypeLoaderFile final { + std::shared_ptr load(MM::Engine& engine, const std::string& path) const; +}; + +} // MM::OpenGL + diff --git a/framework/opengl_renderer/src/mm/opengl/render_tasks/lite_particles2d.cpp b/framework/opengl_renderer/src/mm/opengl/render_tasks/lite_particles2d.cpp new file mode 100644 index 0000000..e6c6d9e --- /dev/null +++ b/framework/opengl_renderer/src/mm/opengl/render_tasks/lite_particles2d.cpp @@ -0,0 +1,779 @@ +#include "./lite_particles2d.hpp" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "../components/lite_particles2d.hpp" + +#include +#include "../lite_particles2d_type.hpp" +#include +#include + +#include + +#include +#ifndef MM_OPENGL_3_GLES + #include +#else + #define TracyGpuContext + #define TracyGpuCollect + #define TracyGpuZone(...) +#endif + +#include + +#include + +namespace MM::OpenGL::RenderTasks { + +// this was glsl code +// TODO: refactor this +// packing helper +static uint16_t pack_float_16bit(const float a, const float pack_mult) { + uint32_t a_bits = uint32_t(abs(a*pack_mult)); + return (a_bits & 0x7fffu) | (uint16_t(a>=0.0 ? 0u : 1u) << 15u); +} + +const float pack_vel_multiplier = 1000.0; + +static float pack_vel(const glm::vec2 vel) { + // ez just mult and save as shorts + return glm::uintBitsToFloat( + (pack_float_16bit(vel.x, pack_vel_multiplier) << 16u) + | pack_float_16bit(vel.y, pack_vel_multiplier) + ); +} + +const float pack_age_multiplier = 10000.0; + +static float pack_age_type(const float age, const uint16_t type) { + return glm::uintBitsToFloat((pack_float_16bit(age, pack_age_multiplier) << 16u) | (type & 0xffffu)); +} + +LiteParticles2D::LiteParticles2D(Engine& engine) { + _particles_0_buffers[0] = std::make_unique>(); + _particles_0_buffers[1] = std::make_unique>(); + + resetBuffers(); + + _tf_vao[0] = std::make_unique(); + _tf_vao[1] = std::make_unique(); + + auto setup_tf_vao = [this](size_t i) { + _tf_vao[i]->bind(); + _particles_0_buffers[i]->bind(); + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0); + _particles_0_buffers[i]->unbind(); + + glEnableVertexAttribArray(0); + + _tf_vao[i]->unbind(); + }; + + setup_tf_vao(0); + setup_tf_vao(1); + + + // rendering + + _render_vao[0] = std::make_unique(); + _render_vao[1] = std::make_unique(); + + auto setup_rend_vao = [this](size_t i) { + size_t next_index = i == 0 ? 1 : 0; + + _render_vao[i]->bind(); + // bc we computed into the other one 0->1 + _particles_0_buffers[next_index]->bind(); + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0); + _particles_0_buffers[next_index]->unbind(); + + glEnableVertexAttribArray(0); + + _render_vao[i]->unbind(); + }; + + setup_rend_vao(0); + setup_rend_vao(1); + + resetTypeBuffers(); + + setupShaderFiles(); + + _tf_shader = ShaderBuilder::start() + .addStageVertexF(engine, vertexPathTF) + .addTransformFeedbackVarying("_out_0") + .addStageFragmentF(engine, fragmentPathTF) // empty stage + .finish(); + assert(static_cast(_tf_shader)); + + _points_shader = ShaderBuilder::start() + .addStageVertexF(engine, vertexPathPoints) + .addStageFragmentF(engine, fragmentPathPoints) + .finish(); + assert(static_cast(_points_shader)); + + _last_time = clock::now(); +} + +LiteParticles2D::~LiteParticles2D(void) { +} + +void LiteParticles2D::uploadParticles(Services::OpenGLRenderer&, Scene& scene) { + ZoneScopedN("MM::OpenGL::RenderTasks::LiteParticles2D::uploadParticles"); + + if (!scene.ctx().contains()) { + // TODO: warn? + return; // nothing to upload + } + + auto& queue = scene.ctx().at(); + + while (!queue.queue.empty()) { + // get range + auto range_size = queue.queue.size(); + size_t range_first = _particle_buffer_next_index; + if (_particle_buffer_next_index + (range_size - 1) >= _particle_buffer_size) { // wrap + range_size = _particle_buffer_size - range_first; + } + + //std::cerr << "s:" << range_size << " f:" << range_first << "\n"; + + //assert(range_first <= range_last); + assert(range_size >= 1); + + size_t curr_buff_index = first_particles_buffer ? 0 : 1; + +#ifdef MM_OPENGL_3_GLES + // i think invalidating the whole buffer, instead of only the range results in undefined behaviour, but webgl wont let me otherwise and it seems to work + const auto gl_access = GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT; +#else + // turns out, amd desktop opengl drivers dont like invalidate buffer (everyone else seems to be ok) + const auto gl_access = GL_MAP_WRITE_BIT; +#endif + + auto* data_0 = _particles_0_buffers[curr_buff_index]->mapRange(range_first, range_size, gl_access); + + for (size_t i = 0; i < range_size; i++) { + auto& p = queue.queue.front(); + + uint16_t type = 0u; + if (_type_map.count(p.type_id)) { + type = _type_map.at(p.type_id); + } else { + SPDLOG_ERROR("unknown type {}, defaulting to zero.", p.type_id); + } + + data_0[i] = glm::vec4( + p.pos.x, p.pos.y, + pack_vel(p.vel), + pack_age_type(p.age, type) + ); + + queue.queue.pop(); + } + + _particles_0_buffers[curr_buff_index]->unmap(); + + _particle_buffer_next_index = (range_first + range_size) % (_particle_buffer_size); + } +} + +void LiteParticles2D::computeParticles(Services::OpenGLRenderer&, Scene& scene) { + ZoneScopedN("MM::OpenGL::RenderTasks::LiteParticles2D::computeParticles"); + using namespace entt::literals; + _tf_shader->bind(); + + { // time + auto newNow = clock::now(); + std::chrono::duration> deltaTime = newNow - _last_time; + _last_time = newNow; + + float time_delta = deltaTime.count() * scene.ctx().at().deltaFactor; + _time += time_delta; + _tf_shader->setUniform1f("_time_delta", time_delta); + _tf_shader->setUniform1f("_time", _time); + } + + auto& rm_t = MM::ResourceManager::ref(); + rm_t.get("MM::LiteParticles2DTypes::Compute"_hs)->bind(0); + + //_tf_shader->setUniform3f("_env_vec", env_vec * env_force); + //_tf_shader->setUniform1f("_noise_force", noise_force); + //_tf_shader->setUniform1f("_dampening", dampening); + + const size_t curr_index = first_particles_buffer ? 0 : 1; + const size_t next_index = first_particles_buffer ? 1 : 0; + + // bind in particles + _tf_vao[curr_index]->bind(); + + // bind out particles + // the order is the same as given to the ShaderBuilder + _particles_0_buffers[next_index]->bindBase(0); + + glEnable(GL_RASTERIZER_DISCARD); // compute only rn + + glBeginTransformFeedback(GL_POINTS); + glDrawArrays(GL_POINTS, 0, _particle_buffer_size); + glEndTransformFeedback(); + + glDisable(GL_RASTERIZER_DISCARD); + + // TODO: move this + glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0); + //glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, 0); + + _tf_vao[curr_index]->unbind(); + _tf_shader->unbind(); + + // TODO: do i need this?? + glFlush(); + + first_particles_buffer = !first_particles_buffer; +} + +void LiteParticles2D::renderParticles(Services::OpenGLRenderer& rs, Scene& scene) { + using namespace entt::literals; + ZoneScopedN("MM::OpenGL::RenderTasks::LiteParticles2D::renderParticles"); + + if (!scene.ctx().contains()) { + return; // nothing to draw + } + + //glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); // dont write to depth + //glEnable(GL_BLEND); + +#ifndef MM_OPENGL_3_GLES + glEnable(GL_PROGRAM_POINT_SIZE); +#endif + + rs.targets[target_fbo]->bind(FrameBufferObject::RW); + + _points_shader->bind(); + + auto& rm_t = MM::ResourceManager::ref(); + rm_t.get("MM::LiteParticles2DTypes::Render"_hs)->bind(0); + + Camera3D& cam = scene.ctx().at(); + _points_shader->setUniformMat4f("_vp", cam.getViewProjection()); + + GLint view_port[4]; + glGetIntegerv(GL_VIEWPORT, view_port); + + // width + _points_shader->setUniform1f("_point_size", view_port[2]/cam.horizontalViewPortSize); + + const size_t curr_index = first_particles_buffer; // 0->1 / 1->0 bc compute phase also switches + _render_vao[curr_index]->bind(); + + glDrawArrays(GL_POINTS, 0, _particle_buffer_size); + + _render_vao[curr_index]->unbind(); + _points_shader->unbind(); + + glDepthMask(GL_TRUE); +} + +void LiteParticles2D::render(Services::OpenGLRenderer& rs, Engine& engine) { + ZoneScopedN("MM::OpenGL::RenderTasks::LiteParticles2D::render"); + + auto* scene_service = engine.tryService(); + if (scene_service == nullptr) { + return; + } + + Scene& scene = scene_service->getScene(); + + uploadParticles(rs, scene); + computeParticles(rs, scene); + renderParticles(rs, scene); +} + +void LiteParticles2D::resetBuffers(void) { + const auto gl_buffer_type = GL_DYNAMIC_COPY; + auto reset_buffer_0 = [this](size_t i) { + auto* data = _particles_0_buffers[i]->map(_particle_buffer_size, gl_buffer_type); + for (size_t x = 0; x < _particle_buffer_size; x++) { + data[x] = glm::vec4{ + 0.f, 0.f, // pos + 0, // vel (pack, but 0 should mean 0) + pack_age_type(2.f, 0) + }; + } + _particles_0_buffers[i]->unmap(); + }; + + reset_buffer_0(0); + reset_buffer_0(1); + + _particle_buffer_next_index = 0; // only matters if resize +} + +void LiteParticles2D::resetTypeBuffers(void) { + using namespace entt::literals; + + // clear + _type_map.clear(); + + // build data + std::vector type_compute_upload_buffer {}; + std::vector type_render_upload_buffer {}; + + // TODO: replace with "default" + { // 0 + // TODO: propper 0 particle + LiteParticles2DType { + { + 0.04f, //age_delta; + + {0.f, 0.f}, //force_vec + + 5.f, //turbulence; + 0.1f, //turbulence_noise_scale; + 0.01f, //turbulence_individuality; + 0.5f, //turbulence_time_scale; + + 1.0f, //dampening; + }, + { + {1.8f, 3.f, 6.f, 1.f}, //color_start; + {2.3f, 1.2f, 0.5f, 1.f}, //color_end; + + 0.1f, //size_start; + 0.02f //size_end; + } + }.upload(type_compute_upload_buffer, type_render_upload_buffer); + } + // defaults + _type_map[0u] = 0u; + _type_map[""_hs] = 0u; + _type_map["0"_hs] = 0u; + _type_map["default"_hs] = 0u; + + auto& rm_lpt = MM::ResourceManager::ref(); + uint16_t curr_idx = 1; // 0 is reserved for now :P + rm_lpt.each([this, &curr_idx, &type_compute_upload_buffer, &type_render_upload_buffer](const auto type_id, const auto& res) { + _type_map[type_id] = curr_idx++; + res->upload(type_compute_upload_buffer, type_render_upload_buffer); + }); + SPDLOG_INFO("have {} LiteParticles2D types.", curr_idx); + + // upload / create textures + // HACK: refactor loader + auto& rm_t = MM::ResourceManager::ref(); + { // compute + auto r = rm_t.reload( + "MM::LiteParticles2DTypes::Compute", + GL_R32F, + LiteParticles2DType::Compute::_member_count, curr_idx, + GL_RED, + GL_FLOAT + ); + assert(r); + + // and re-"create" + rm_t.get("MM::LiteParticles2DTypes::Compute"_hs)->bind(0); // hack + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_R32F, + LiteParticles2DType::Compute::_member_count, curr_idx, + 0, + GL_RED, + GL_FLOAT, + type_compute_upload_buffer.data() + ); + // just in case + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + + { // render + auto r = rm_t.reload( + "MM::LiteParticles2DTypes::Render", + GL_R32F, + LiteParticles2DType::Render::_member_count, curr_idx, + GL_RED, + GL_FLOAT + ); + assert(r); + + // and re-"create" + rm_t.get("MM::LiteParticles2DTypes::Render"_hs)->bind(0); // hack + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_R32F, + LiteParticles2DType::Render::_member_count, curr_idx, + 0, + GL_RED, + GL_FLOAT, + type_render_upload_buffer.data() + ); + // just in case + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } +} + +void LiteParticles2D::setupShaderFiles(void) { + FS_CONST_MOUNT_FILE(commonPathTF, +R"( + +#include "/shaders/builtin/noise.glsl" + +// variants, based on offset, might be faster +vec2 var_noise22(vec2 x) { + return vec2(noise12(x), noise12(x + 13037.0)); +} +vec2 var_noise23(vec3 x) { + return vec2(noise13(x), noise13(x + 13037.0)); +} +vec2 var_noise24(vec4 x) { + return vec2(noise14(x), noise14(x + 13037.0)); +} + +// packing helper +uint pack_float_16bit(in float a, in float pack_mult) { + uint a_bits = uint(abs(a*pack_mult)); + + return (a_bits & 0x7fffu) | (uint(a>=0.0 ? 0u : 1u) << 15u); +} + +float unpack_float_16bit(in uint a, in float pack_mult) { + return (float(a & 0x7fffu) / pack_mult) * ((a >> 15u) != 0u ? -1.0 : 1.0); +} + +const float pack_vel_multiplier = 1000.0; + +float pack_vel(in vec2 vel) { + return uintBitsToFloat( + (pack_float_16bit(vel.x, pack_vel_multiplier) << 16u) + | pack_float_16bit(vel.y, pack_vel_multiplier) + ); +} + +vec2 unpack_vel(in float pack) { + uint pack_bits = floatBitsToUint(pack); + return vec2( + unpack_float_16bit(pack_bits >> 16u, pack_vel_multiplier), + unpack_float_16bit(pack_bits & 0xffffu, pack_vel_multiplier) + ); +} + +const float pack_age_multiplier = 10000.0; + +float pack_age_type(in float age, in uint type) { + return uintBitsToFloat( + (pack_float_16bit(age, pack_age_multiplier) << 16u) + | (type & 0xffffu) + ); +} + +)") + FS_CONST_MOUNT_FILE(vertexPathTF, +GLSL_VERSION_STRING +R"( +#ifdef GL_ES + precision highp float; + precision highp int; +#endif + +#include "./common.glsl" + +uniform float _time_delta; +uniform float _time; + +uniform sampler2D _type_buffer; + +layout(location = 0) in vec4 _in_0; + +out vec4 _out_0; + +void set_out(in vec2 pos, in vec2 vel, in float age, in uint type) { + _out_0 = vec4( + pos.x, pos.y, + pack_vel(vel), + pack_age_type(age, type) + ); +} + +void get_in(out vec2 pos, out vec2 vel, out float age, out uint type) { + pos = _in_0.xy; + vel = unpack_vel(_in_0.z); + + uint age_type_bits = floatBitsToUint(_in_0.w); + age = unpack_float_16bit(age_type_bits >> 16u, pack_age_multiplier); + type = age_type_bits & 0xffffu; +} + +struct sim_config_type { + highp float age_delta; + + //highp float gravity; + vec2 force_vec; + + highp float turbulence; + highp float turbulence_noise_scale; + highp float turbulence_individuality; + highp float turbulence_time_scale; + + highp float dampening; +}; + +vec2 fetch_vec2(in uint offset, in uint type) { + return vec2( + texelFetch(_type_buffer, ivec2(offset+0u, type), 0).r, + texelFetch(_type_buffer, ivec2(offset+1u, type), 0).r + ); +} + +sim_config_type fetch_type_config(uint type) { + return sim_config_type( + texelFetch(_type_buffer, ivec2(0, type), 0).r, + fetch_vec2(1u, type), + texelFetch(_type_buffer, ivec2(3, type), 0).r, + texelFetch(_type_buffer, ivec2(4, type), 0).r, + texelFetch(_type_buffer, ivec2(5, type), 0).r, + texelFetch(_type_buffer, ivec2(6, type), 0).r, + texelFetch(_type_buffer, ivec2(7, type), 0).r + ); +} + +vec2 calc_turbulence_vec( + in vec2 pos, + in float turbulence, + in float turbulence_noise_scale, + in float turbulence_individuality, + in float turbulence_time_scale +) { + vec2 turbulence_vec = + ( + noise24(vec4( + pos * turbulence_noise_scale, + float(gl_VertexID & 0x0fff) * turbulence_individuality, + _time * turbulence_time_scale + )) + - vec2(0.5) + ) + * 2.0 + * turbulence + ; + + return turbulence_vec; +} + +void calc_particle_sim( +// inout (particle mem) + inout vec2 pos, + inout vec2 vel, + + inout float age, + inout uint type, + +// type specific config + in sim_config_type conf +) { + vec2 turbulence_vec = calc_turbulence_vec( + pos, + conf.turbulence, + conf.turbulence_noise_scale, + conf.turbulence_individuality, + conf.turbulence_time_scale + ); + + vel = (vel + ( + turbulence_vec + + //vec2(0.0, -10.0) * conf.gravity + conf.force_vec + ) * _time_delta); + + // TODO: this does not behave correctly + // TODO: experiment with formula + vel = mix(vel, vec2(0.0), conf.dampening * _time_delta); + + pos = pos + (vel * _time_delta); + + age = min(age + (conf.age_delta * _time_delta), 2.0); +} + +void main() { + vec2 pos; + vec2 vel; + float age; + uint type; + get_in(pos, vel, age, type); + + sim_config_type conf = fetch_type_config(type); + + calc_particle_sim( + pos, + vel, + age, + type, + + conf + ); + + set_out(pos, vel, age, type); +})") + + FS_CONST_MOUNT_FILE(fragmentPathTF, +GLSL_VERSION_STRING +R"( +#ifdef GL_ES + precision mediump float; +#endif + +// i am never executed + +out vec4 _out_color; + +void main() { + _out_color = vec4(1.0); +})") + + FS_CONST_MOUNT_FILE(vertexPathPoints, +GLSL_VERSION_STRING +R"( +#ifdef GL_ES + precision highp float; + precision highp int; +#endif + +#include "./common.glsl" + +uniform mat4 _vp; +uniform float _point_size; + +uniform sampler2D _type_buffer; + +layout(location = 0) in vec4 _in_0; + +void get_in(out vec2 pos, out vec2 vel, out float age, out uint type) { + pos = _in_0.xy; + vel = unpack_vel(_in_0.z); + + uint age_type_bits = floatBitsToUint(_in_0.w); + age = unpack_float_16bit(age_type_bits >> 16u, pack_age_multiplier); + type = age_type_bits & 0xffffu; +} + +out vec4 _frag_color; +flat out int _vertex_id; + +struct config_type { + highp vec4 color_start; + highp vec4 color_end; + + highp float size_start; + highp float size_end; +}; + +vec4 fetch_vec4(in uint offset, in uint type) { + return vec4( + texelFetch(_type_buffer, ivec2(offset+0u, type), 0).r, + texelFetch(_type_buffer, ivec2(offset+1u, type), 0).r, + texelFetch(_type_buffer, ivec2(offset+2u, type), 0).r, + texelFetch(_type_buffer, ivec2(offset+3u, type), 0).r + ); +} + +config_type fetch_config_type(uint type) { + return config_type( + fetch_vec4(0u, type), + fetch_vec4(4u, type), + texelFetch(_type_buffer, ivec2(8, type), 0).r, + texelFetch(_type_buffer, ivec2(9, type), 0).r + ); +} + +void main() { + vec2 in_pos; + vec2 in_vel; + + float in_age; + uint in_type; + + get_in(in_pos, in_vel, in_age, in_type); + + // skip dead + if (in_age > 1.0) { + _frag_color = vec4(1.0, 0.0, 1.0, 0.0); // so it does not get rendered (alpha) + gl_Position.z = 100000000000.0; // clip (does not allways work <.<) + // gl_PointSize = 0.0; // ub + return; + } + + config_type conf = fetch_config_type(in_type); + + //_frag_color = mix(conf.color_start, conf.color_end, in_age); + _frag_color = mix(conf.color_start, conf.color_end, smoothstep(0.0, 1.0, in_age)); + + gl_Position = _vp * vec4(in_pos, 0.0, 1.0); + + // TODO: fix before merge + gl_Position.z = -1.0; // hack for ortho ?? + + gl_PointSize = max(mix(conf.size_start, conf.size_end, in_age) * _point_size, 1.0); + + _vertex_id = gl_VertexID; +})") + + FS_CONST_MOUNT_FILE(fragmentPathPoints, +GLSL_VERSION_STRING +R"( +#ifdef GL_ES + precision mediump float; +#endif + +in vec4 _frag_color; +flat in int _vertex_id; + +out vec4 _out_color; + +bool should_discard(vec2 screen_pos, float alpha) { + const int dither[8 * 8] = int[8 * 8]( + 0, 32, 8, 40, 2, 34, 10, 42, + 48, 16, 56, 24, 50, 18, 58, 26, + 12, 44, 4, 36, 14, 46, 6, 38, + 60, 28, 52, 20, 62, 30, 54, 22, + 3, 35, 11, 43, 1, 33, 9, 41, + 51, 19, 59, 27, 49, 17, 57, 25, + 15, 47, 7, 39, 13, 45, 5, 37, + 63, 31, 55, 23, 61, 29, 53, 21 + ); + + int mat_value = dither[int(screen_pos.x)%8 + 8 * (int(screen_pos.y)%8)]; + float n_mat_value = float(mat_value) * (1.0/64.0); + + return n_mat_value < 1.0 - alpha; +} + +void main() { + if (should_discard(vec2(gl_FragCoord.x + float(_vertex_id%8), gl_FragCoord.y + float(_vertex_id%7)), _frag_color.a)) { + discard; + } + + _out_color = vec4(_frag_color.rgb, 1.0); +})") + +} + +} // MM::OpenGL::RenderTasks + diff --git a/framework/opengl_renderer/src/mm/opengl/render_tasks/lite_particles2d.hpp b/framework/opengl_renderer/src/mm/opengl/render_tasks/lite_particles2d.hpp new file mode 100644 index 0000000..719f7c9 --- /dev/null +++ b/framework/opengl_renderer/src/mm/opengl/render_tasks/lite_particles2d.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include +#include + +#include + +// TODO: make the renderer provide a time delta +#include +#include +#include + +// fwd +namespace MM::OpenGL { + class Shader; + class Buffer; + class VertexArrayObject; + + template + class InstanceBuffer; +} + +namespace MM::OpenGL::RenderTasks { + + class LiteParticles2D : public RenderTask { + private: + std::shared_ptr _tf_shader; + std::shared_ptr _points_shader; + + std::unordered_map _type_map {}; // maps id to "index" in type texture + + // double buffered + bool first_particles_buffer {true}; + std::unique_ptr _tf_vao[2]; + std::unique_ptr _render_vao[2]; + + // vec2 pos (2 floats) + // half vec2 vel (1 float) + // fixedpoint age + uint16 type (1 float) + std::unique_ptr> _particles_0_buffers[2]; + + uint32_t _particle_buffer_size {10'000}; + size_t _particle_buffer_next_index {0}; + + using clock = std::chrono::high_resolution_clock; + std::chrono::time_point _last_time; + float _time {0}; + + public: + //glm::vec3 env_vec{0, 1, 0}; + //float env_force{0.3}; + //float noise_force{0.5}; + //float dampening{0.99}; + + LiteParticles2D(Engine& engine); + ~LiteParticles2D(void); + + const char* name(void) override { return "LiteParticles2D"; } + + void uploadParticles(Services::OpenGLRenderer& rs, Scene& scene); + void computeParticles(Services::OpenGLRenderer& rs, Scene& scene); + void renderParticles(Services::OpenGLRenderer& rs, Scene& scene); + void render(Services::OpenGLRenderer& rs, Engine& engine) override; + + void resetBuffers(void); + void resetTypeBuffers(void); + + public: + const char* commonPathTF {"shader/lite_particles2d/common.glsl"}; + + const char* vertexPathTF {"shader/lite_particles2d/tf_vert.glsl"}; + const char* fragmentPathTF {"shader/lite_particles2d/tf_frag.glsl"}; // "empty" bc of webgl (es) + + const char* vertexPathPoints {"shader/lite_particles2d/point_vert.glsl"}; + const char* fragmentPathPoints {"shader/lite_particles2d/point_frag.glsl"}; + + std::string target_fbo {"display"}; + + private: + void setupShaderFiles(void); + }; + +} // MM::OpenGL::RenderTasks + diff --git a/framework/opengl_renderer/test/CMakeLists.txt b/framework/opengl_renderer/test/CMakeLists.txt index 9648f84..15db541 100644 --- a/framework/opengl_renderer/test/CMakeLists.txt +++ b/framework/opengl_renderer/test/CMakeLists.txt @@ -168,7 +168,6 @@ target_link_libraries(hdr_bloom_pipeline_example opengl_renderer_s organizer_scene clear_render_task - blit_fb_render_task simple_rect_render_task #bloom_extraction_render_task @@ -187,3 +186,27 @@ target_link_libraries(hdr_bloom_pipeline_example add_test(NAME hdr_bloom_pipeline_example COMMAND hdr_bloom_pipeline_example) +################# lite_particles2d (the test uses bloom) + +add_executable(lite_particles2d_test ./lite_particles2d.cpp) + +target_link_libraries(lite_particles2d_test + opengl_renderer_s + organizer_scene + clear_render_task + #simple_rect_render_task + lite_particles2d # not only rendertask + + bloom + composition_render_task + + #simple_velocity_system + #transform_system + + random + + gtest_main +) + +add_test(NAME lite_particles2d_test COMMAND lite_particles2d_test) + diff --git a/framework/opengl_renderer/test/lite_particles2d.cpp b/framework/opengl_renderer/test/lite_particles2d.cpp new file mode 100644 index 0000000..a537f0c --- /dev/null +++ b/framework/opengl_renderer/test/lite_particles2d.cpp @@ -0,0 +1,371 @@ +#include + +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +// ctx +#include +#include +#include + +// components +#include + +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +using namespace entt::literals; + +namespace Components { + +// or what ever +struct LiteParticles2DEmitter { + float age {0.f}; // this changes each tick, if >=1, spawn and reset to 0 + float age_delta {1.f}; // times per sec + + MM::ScalarRange2 particle_count {1u, 1u}; + + // particles data + + std::string p_type {}; + + MM::ScalarRange2 p_pos_x {0.f, 0.f}; + MM::ScalarRange2 p_pos_y {0.f, 0.f}; + + MM::ScalarRange2 p_dir {0.f, 0.f}; // 0-1, not rad + MM::ScalarRange2 p_dir_force {0.f, 0.f}; + + MM::ScalarRange2 p_age {0.f, 0.f}; +}; + +} // Components + +namespace Systems { + +void lite_particles2d_emit( + entt::view> view, + const MM::Components::TimeDelta& td, + MM::Components::LiteParticles2DUploadQueue& lp_uq, + MM::Random::SRNG& rng +) { + view.each([&lp_uq, &td, &rng](const auto& pos, Components::LiteParticles2DEmitter& lpe) { + lpe.age += lpe.age_delta * td.tickDelta; + + if (lpe.age < 1.f) { + return; + } + + lpe.age = 0.f; // TODO: dont discard ? + + const auto type = entt::hashed_string::value(lpe.p_type.c_str()); + + const size_t count = rng.range(lpe.particle_count); + for (size_t i = 0; i < count; i++) { + float dir = rng.range(lpe.p_dir) * glm::two_pi(); + lp_uq.queue.push(MM::Components::LiteParticle2D{ + type, // type + + //{rng.negOneToOne() * 30.f, rng.negOneToOne() * 30.f}, // pos + pos.pos + /*lpe.pos_offset +*/ glm::vec2{rng.range(lpe.p_pos_x), rng.range(lpe.p_pos_y)}, // pos + glm::vec2{glm::cos(dir), glm::sin(dir)} * rng.range(lpe.p_dir_force), // vel + + rng.range(lpe.p_age) // age + }); + } + }); +} + +} // Systems + +const char* argv0; + +static void setup_textures(MM::Engine& engine) { + auto& rm_t = MM::ResourceManager::ref(); + auto [w, h] = engine.getService().getWindowSize(); + + // we dont have a gbuffer in this example + { // gbuffer + // depth +#ifdef MM_OPENGL_3_GLES + rm_t.reload( + "depth", + GL_DEPTH_COMPONENT24, // d16 is the onlyone for gles 2 (TODO: test for 3) + w, h, + GL_DEPTH_COMPONENT, GL_UNSIGNED_INT + ); +#else + rm_t.reload( + "depth", + GL_DEPTH_COMPONENT32F, // TODO: stencil? + w, h, + GL_DEPTH_COMPONENT, GL_FLOAT + ); +#endif + } + + const float render_scale = 1.f; + + // hdr color gles3 / webgl2 + rm_t.reload( + "hdr_color", + GL_RGBA16F, + w * render_scale, h * render_scale, + GL_RGBA, + GL_HALF_FLOAT + ); + { // filter + rm_t.get("hdr_color"_hs)->bind(0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } +} + +static void setup_fbos(MM::Engine& engine) { + auto& rs = engine.getService(); + auto& rm_t = MM::ResourceManager::ref(); + + const float render_scale = 1.f; + + rs.targets["game_view"] = MM::OpenGL::FBOBuilder::start() + .attachTexture(rm_t.get("hdr_color"_hs), GL_COLOR_ATTACHMENT0) + .attachTexture(rm_t.get("depth"_hs), GL_DEPTH_ATTACHMENT) + .setResizeFactors(render_scale, render_scale) + .setResize(true) + .finish(); + assert(rs.targets["game_view"]); +} + +void setup_particles_types(MM::Engine& engine) { + auto& lpt_rm = MM::ResourceManager::ref(); + + lpt_rm.load("MM::spark1", +R"({ + "compute": { + "age_delta": 2.0, + "force_vec": { "x": 0.0, "y": -5.0 }, + "turbulence": 1.0, + "turbulence_individuality": 1.0, + "turbulence_noise_scale": 1.0, + "turbulence_time_scale": 1.0, + "dampening": 1.0 + }, + "render": { + "color_start": { + "x": 6.0, + "y": 6.0, + "z": 1.8, + "w": 1.0 + }, + "color_end": { + "x": 1.5, + "y": 1.0, + "z": 0.3, + "w": 1.0 + }, + "size_start": 0.01, + "size_end": 0.002 + } +})"_json); + + // mount into vfx + FS_CONST_MOUNT_FILE("/particles/lite_particles2d/fire1.json", +R"({ + "compute": { + "age_delta": 1.0, + "force_vec": { "x": 0.0, "y": 5.0 }, + "turbulence": 5.0, + "turbulence_individuality": 1.0, + "turbulence_noise_scale": 1.0, + "turbulence_time_scale": 1.0, + "dampening": 0.0 + }, + "render": { + "color_start": { + "x": 3.0, + "y": 2.1, + "z": 1.5, + "w": 0.8 + }, + "color_end": { + "x": 3.0, + "y": 1.1, + "z": 1.0, + "w": 0.0 + }, + "size_start": 0.15, + "size_end": 0.4 + } +})"); + // and load it + lpt_rm.load("MM::fire1", engine, "/particles/lite_particles2d/fire1.json"); + +} + +TEST(hdr_bloom_pipeline, it) { + MM::Engine engine; + + auto& sdl_ss = engine.addService(); + ASSERT_TRUE(engine.enableService()); + + sdl_ss.createGLWindow("hdr_bloom_pipeline_example", 1280, 720); + + engine.addService(); + ASSERT_TRUE(engine.enableService()); + + bool provide_ret = engine.provide(); + ASSERT_TRUE(provide_ret); + auto& scene = engine.tryService()->getScene(); + + engine.addService(argv0, "hdr_bloom_pipeline_example"); + ASSERT_TRUE(engine.enableService()); + + auto& rs = engine.addService(); + ASSERT_TRUE(engine.enableService()); + + // load particle types + // before addRenderTask + // OR + // call LiteParticle2D::resetTypeBuffers() + setup_particles_types(engine); + + { // setup rendering + // TODO: split vertically + setup_textures(engine); + setup_fbos(engine); + + // clear + auto& clear_opaque = rs.addRenderTask(engine); + clear_opaque.target_fbo = "game_view"; + // clears all color attachments + clear_opaque.r = 0.f; + clear_opaque.g = 0.f; + clear_opaque.b = 0.f; + clear_opaque.a = 1.f; + clear_opaque.mask = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT; + + { // render + //MM::OpenGL::RenderTasks::SimpleRect& bsrr_rend = rs.addRenderTask(engine); + //bsrr_rend.target_fbo = "game_view"; + auto& lprt = rs.addRenderTask(engine); + lprt.target_fbo = "game_view"; + } + + // rn does rt too + MM::OpenGL::setup_bloom(engine); + + // not part of setup_bloom + auto& comp = rs.addRenderTask(engine); + comp.color_tex = "hdr_color"; + comp.bloom_tex = "blur_tmp1"; + comp.target_fbo = "display"; + } + + + // setup scene + + auto& cam = scene.ctx().emplace(); + cam.horizontalViewPortSize = 30.f; + cam.setOrthographic(); + cam.updateView(); + + scene.ctx().emplace(); + + scene.ctx().emplace(42u); + + // setup v system + auto& org = scene.ctx().emplace(); + org.emplace("lite_particles2d_emit"); + + // HACK: instead you would switch to this scene + engine.getService().updateOrganizerVertices(scene); + + { // default + auto e = scene.create(); + auto& p = scene.emplace(e); + p.pos.x = -10.f; + + auto& lpe = scene.emplace(e); + lpe.age_delta = 60.f; + lpe.particle_count = {1, 1}; + lpe.p_type = "default"; + lpe.p_dir = {-0.1f, +0.1f}; + lpe.p_dir_force = {0.f, 8.f}; + lpe.p_age = {0.f, 0.7f}; + } + + { // sparks + auto e = scene.create(); + auto& p = scene.emplace(e); + p.pos.x = 0.f; + + auto& lpe = scene.emplace(e); + lpe.age_delta = 1.f; + lpe.particle_count = {25, 40}; + lpe.p_type = "MM::spark1"; + lpe.p_dir = {0.f, 1.f}; + lpe.p_dir_force = {0.f, 1.f}; // m/s + lpe.p_age = {0.f, 0.7f}; + } + + { // fire + auto e = scene.create(); + auto& p = scene.emplace(e); + p.pos.x = 10.f; + + auto& lpe = scene.emplace(e); + lpe.age_delta = 60.f; + lpe.particle_count = {1, 1}; + lpe.p_type = "MM::fire1"; + lpe.p_dir = {0.f, 1.f}; + lpe.p_dir_force = {0.f, 1.f}; + lpe.p_age = {0.f, 0.5f}; + } + + engine.run(); +} + +int main(int argc, char** argv) { + argv0 = argv[0]; + + ::testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} +