diff --git a/examples/fire.cpp b/examples/fire.cpp new file mode 100644 index 00000000..04d0439c --- /dev/null +++ b/examples/fire.cpp @@ -0,0 +1,326 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace psemek; + +static char const vertex_source[] = +R"(#version 330 + +uniform mat4 u_transform; +uniform vec3 u_camera_pos; + +layout (location = 0) in vec4 in_position; + +layout (location = 1) in vec3 in_instance_pos; +layout (location = 2) in vec3 in_instance_z; +layout (location = 3) in float in_instance_size; +layout (location = 4) in vec2 in_instance_noise_offset; + +out vec2 texcoord; +out vec2 noise_offset; +out float z; + +void main() +{ + vec3 instance_y = u_camera_pos - in_instance_pos; + instance_y -= dot(in_instance_z, instance_y) * in_instance_z; + instance_y = -normalize(instance_y); + vec3 instance_x = cross(instance_y, in_instance_z); + + mat4 transform; + transform[0] = vec4(instance_x, 0.0); + transform[1] = vec4(instance_y, 0.0); + transform[2] = vec4(in_instance_z, 0.0); + transform[3] = vec4(in_instance_pos, 1.0); + + gl_Position = u_transform * (transform * in_position); + + texcoord = in_position.xz * 0.5 + vec2(0.5, 0.5); + noise_offset = in_instance_noise_offset; + z = in_position.z; +} +)"; + +static char const fragment_source[] = +R"(#version 330 + +uniform sampler2D u_texture; +uniform sampler2D u_noise_texture; + +uniform float u_time; + +in vec2 texcoord; +in vec2 noise_offset; +in float z; + +out vec4 out_color; + +void main() +{ + vec2 tc = texcoord + (2.0 * texture(u_noise_texture, texcoord + noise_offset * u_time).xy - vec2(1.0, 1.0)) * 0.1 * z; + out_color = texture(u_texture, tc); +} +)"; + +struct candle_renderer +{ + candle_renderer(); + + void add(geom::point const & pos, geom::vector const & dir, float size); + + void render(geom::camera const & camera, float time); + +private: + struct candle + { + geom::point pos; + geom::vector dir; + float size; + geom::vector noise_offset; + }; + + std::vector candles_; + bool instances_need_update_ = false; + + gfx::program program_{vertex_source, fragment_source}; + + gfx::instanced_mesh mesh_; + + gfx::texture_2d texture_; + gfx::texture_2d noise_texture_; + + void update_instances(); +}; + +candle_renderer::candle_renderer() +{ + std::vector> vertices; + vertices.push_back({-1.f, 0.f, -1.f}); + vertices.push_back({ 1.f, 0.f, -1.f}); + vertices.push_back({ 1.f, 0.f, 1.f}); + vertices.push_back({-1.f, 0.f, -1.f}); + vertices.push_back({ 1.f, 0.f, 1.f}); + vertices.push_back({-1.f, 0.f, 1.f}); + + mesh_.setup, gfx::instanced>, gfx::instanced>, gfx::instanced, gfx::instanced>>(); + mesh_.load(vertices, gl::TRIANGLES); + + geom::vector c0 {1.f, 0.99f, 0.98f, 1.f}; + geom::vector c1 {1.f, 0.4f, 0.f, 0.75f}; + geom::vector c2 {c1[0], c1[1], c1[2], 0.f}; + + geom::gradient gy{ + std::make_pair(0.f, 0.f), + geom::easing_type::quadratic_in, + std::make_pair(1.f, 2.f), + }; + + geom::gradient> gc + { + std::make_pair(0.1f, c0), + geom::easing_type::linear, + std::pair{0.8f, c1}, + geom::easing_type::cubic, + std::pair{1.2f, c2} + }; + + { + gfx::pixmap_rgba pm({512, 512}, {0, 0, 0, 0}); + for (auto idx : pm.indices()) + { + float x = 2.f * (idx[0] + 0.5f) / pm.width() - 1.f; + float y = 2.f * (idx[1] + 0.5f) / pm.height() - 1.f; + + float d = 10.f; + + float size = 0.1f; + + if (y >= 0) + { + d = std::abs(x) / size + gy(y); + } + else + { + d = std::sqrt(x * x + y * y) / size; + } + + pm(idx) = gfx::to_coloru8(gc(d)); + } + texture_.load(pm); + texture_.linear_filter(); + texture_.anisotropy(); + texture_.generate_mipmap(); + texture_.clamp(); + } + + { + pcg::generator rng; + pcg::uniform_sphere_vector_distribution d; + + util::array, 2> grad({16, 16}); + + for (auto & v : grad) v = d(rng); + pcg::perlin perlinx(grad, pcg::seamless); + + for (auto & v : grad) v = d(rng); + pcg::perlin perliny(grad, pcg::seamless); + + gfx::basic_pixmap> pm({512, 512}); + for (auto idx : pm.indices()) + { + float x = (0.5f + idx[0]) / pm.width(); + float y = (0.5f + idx[1]) / pm.height(); + + pm(idx) = gfx::to_coloru8(geom::vector{perlinx({x, y}), perliny({x, y})}); + } + noise_texture_.load(pm); + noise_texture_.linear_filter(); + noise_texture_.anisotropy(); + noise_texture_.generate_mipmap(); + noise_texture_.repeat(); + } +} + +void candle_renderer::add(geom::point const & pos, geom::vector const & dir, float size) +{ + pcg::generator rng{pcg::random_device{}}; + pcg::uniform_sphere_vector_distribution d; + geom::vector noise_offset; + while (true) + { + noise_offset = d(rng); + if (std::abs(noise_offset[0]) < 0.5f) + break; + } + + if (noise_offset[1] > 0.f) + noise_offset[1] *= -1.f; + + candles_.push_back({pos, dir, size, noise_offset * 0.5f}); + instances_need_update_ = true; +} + +void candle_renderer::render(geom::camera const & camera, float time) +{ + if (instances_need_update_) update_instances(); + + program_.bind(); + program_["u_transform"] = camera.transform(); + program_["u_camera_pos"] = camera.position(); + program_["u_texture"] = 0; + program_["u_noise_texture"] = 1; + program_["u_time"] = time; + gl::ActiveTexture(gl::TEXTURE0); + texture_.bind(); + gl::ActiveTexture(gl::TEXTURE1); + noise_texture_.bind(); + (void)time; + mesh_.draw(); +} + +void candle_renderer::update_instances() +{ + struct instance + { + geom::point pos; + geom::vector dir; + float size; + geom::vector noise_offset; + }; + + std::vector instances; + instances.reserve(candles_.size()); + + for (candle const & c: candles_) + { + instances.push_back({c.pos, c.dir, c.size, c.noise_offset}); + } + + mesh_.load_instance(instances); +} + +struct fire_app + : app::app +{ + geom::spherical_camera camera; + + candle_renderer candles; + + util::clock> clock; + float time = 0.f; + + fire_app() + : app("Fire") + { + camera.fov_y = geom::rad(45.f); + camera.near_clip = 0.01f; + camera.far_clip = 1000.f; + camera.target = {0.f, 0.f, 0.f}; + camera.elevation_angle = 0.f; + camera.azimuthal_angle = 0.f; + camera.distance = 4.f; + + candles.add({0.f, 0.f, 0.f}, {0.f, 0.f, 1.f}, 1.f); + } + + void on_resize(int width, int height) override + { + app::on_resize(width, height); + camera.set_fov(camera.fov_y, (1.f * width) / height); + } + + void on_mouse_move(int x, int y, int dx, int dy) override + { + app::on_mouse_move(x, y, dx, dy); + + if (is_middle_button_down()) + { + camera.azimuthal_angle -= dx * 0.01f; + camera.elevation_angle += dy * 0.01f; + } + } + + void on_mouse_wheel(int delta) override + { + app::on_mouse_wheel(delta); + camera.distance *= std::pow(0.8f, delta); + } + + void update() override + { + if (!is_key_down(SDLK_SPACE)) + time += clock.restart().count(); + } + + void render() override + { + gl::ClearColor(0.f, 0.f, 0.f, 1.f); + gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT); + + gl::Enable(gl::BLEND); + gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); + + candles.render(camera, time); + } + +}; + +int main() +{ + return app::main(); +}