Skip to content

Falling sand

Romain Milbert edited this page Aug 28, 2023 · 5 revisions

⚠️ This program may not run as shown on your own computer: since everything is computed within a single texture, the result may vary (and can be broken entirely) depending on your driver; to avoid this, two buffers should be used in turn. It would be possible to read from and write to an 32-bit integer image in the compute shader to make use of atomic operations, and output color from the fragment shader. This however would most likely not solve random accesses between neighbouring pixels.


As the time of writing (2022-11-23, commit ff435d0), the following file give the result shown in this video:

RaZ - Falling sand GIF

Using this code, you can perform several actions:

  • Add "sand" pixels with the left mouse click;
  • Add "stone" pixels (which remain in place) with the right mouse click;
  • Add "dirt" pixels with the middle mouse click.

main.cpp

#include <RaZ/Application.hpp>
#include <RaZ/Math/Transform.hpp>
#include <RaZ/Render/Camera.hpp>
#include <RaZ/Render/RenderSystem.hpp>

constexpr int sceneWidth  = 1280;
constexpr int sceneHeight = 720;

int main() {
  Raz::Application app;
  Raz::World& world = app.addWorld(1);

  auto& render = world.addSystem<Raz::RenderSystem>(sceneWidth, sceneHeight, "RaZ");
  render.getWindow().addKeyCallback(Raz::Keyboard::ESCAPE, [&app] (float) noexcept { app.quit(); });
  render.getWindow().setCloseCallback([&app] () noexcept { app.quit(); });

  world.addEntityWithComponent<Raz::Transform>().addComponent<Raz::Camera>(sceneWidth, sceneHeight);

  Raz::Texture2DPtr texture = Raz::Texture2D::create(sceneWidth, sceneHeight, Raz::TextureColorspace::RGBA, Raz::TextureDataType::BYTE);
  Raz::Renderer::bindImageTexture(0, texture->getIndex(), 0, false, 0, Raz::ImageAccess::READ_WRITE, Raz::ImageInternalFormat::RGBA8);

  Raz::ComputeShaderProgram compute(Raz::ComputeShader::loadFromSource(R"(
    layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;

    layout(rgba8, binding = 0) uniform image2D uniBuffer;
    uniform bool uniMouseLeftClick   = false;
    uniform bool uniMouseRightClick  = false;
    uniform bool uniMouseMiddleClick = false;
    uniform ivec2 uniMousePos;

    const vec3 dirtColor  = vec3(0.608, 0.463, 0.325);
    const vec3 sandColor  = vec3(0.761, 0.698, 0.502);
    const vec3 stoneColor = vec3(0.35);

    void main() {
      ivec2 pixelCoords = ivec2(gl_GlobalInvocationID.xy);
      vec2 invImgDims   = 1.0 / vec2(imageSize(uniBuffer));

      vec3 currentFrag = imageLoad(uniBuffer, pixelCoords).rgb;

      if (currentFrag == vec3(0.0) && uniMousePos == pixelCoords) {
        if (uniMouseLeftClick) {
          currentFrag = sandColor;
        } else if (uniMouseRightClick) {
          imageStore(uniBuffer, pixelCoords + ivec2(-1, 0), vec4(stoneColor, 1.0));
          imageStore(uniBuffer, pixelCoords + ivec2(1, 0), vec4(stoneColor, 1.0));
          imageStore(uniBuffer, pixelCoords + ivec2(0, -1), vec4(stoneColor, 1.0));
          imageStore(uniBuffer, pixelCoords + ivec2(0, 1), vec4(stoneColor, 1.0));
          imageStore(uniBuffer, pixelCoords + ivec2(-1, -1), vec4(stoneColor, 1.0));
          imageStore(uniBuffer, pixelCoords + ivec2(1, 1), vec4(stoneColor, 1.0));
          imageStore(uniBuffer, pixelCoords + ivec2(1, -1), vec4(stoneColor, 1.0));
          imageStore(uniBuffer, pixelCoords + ivec2(-1, 1), vec4(stoneColor, 1.0));
          currentFrag = stoneColor;
        } else if (uniMouseMiddleClick) {
          currentFrag = dirtColor;
        }

        imageStore(uniBuffer, pixelCoords, vec4(currentFrag, 1.0));
        return;
      }

      if (abs(dot(currentFrag, currentFrag) - dot(stoneColor, stoneColor)) <= 0.01)
        return;

      ivec2 belowCoords = pixelCoords - ivec2(0, 1);
      vec3 belowFrag    = imageLoad(uniBuffer, belowCoords).rgb;

      if (belowFrag == vec3(0.0) && belowCoords.y >= 0) {
        imageStore(uniBuffer, belowCoords, vec4(currentFrag, 1.0));
        currentFrag = vec3(0.0);
      } else {
        belowCoords = pixelCoords - ivec2(0, 5);
        belowFrag   = imageLoad(uniBuffer, belowCoords).rgb;

        if (belowFrag != vec3(0.0) && belowCoords.y >= 5) {
          if (imageLoad(uniBuffer, pixelCoords - ivec2(1, 1)).rgb == vec3(0.0)) {
            imageStore(uniBuffer, pixelCoords - ivec2(1, 1), vec4(currentFrag, 1.0));
            currentFrag = vec3(0.0);
          } else if (imageLoad(uniBuffer, pixelCoords - ivec2(-1, 1)).rgb == vec3(0.0)) {
            imageStore(uniBuffer, pixelCoords - ivec2(-1, 1), vec4(currentFrag, 1.0));
            currentFrag = vec3(0.0);
          }
        }
      }

      imageStore(uniBuffer, pixelCoords, vec4(currentFrag, 1.0));
    }
  )"));

  render.getWindow().addMouseButtonCallback(Raz::Mouse::LEFT_CLICK, [&compute, &render] (float) {
    compute.use();
    compute.sendUniform("uniMouseLeftClick", true);

    const Raz::Vec2f mousePos = render.getWindow().recoverMousePosition();
    compute.sendUniform("uniMousePos", Raz::Vec2i(static_cast<int>(mousePos.x()), sceneHeight - static_cast<int>(mousePos.y())));
  }, Raz::Input::ALWAYS, [&compute] () {
    compute.use();
    compute.sendUniform("uniMouseLeftClick", false);
  });

  render.getWindow().addMouseButtonCallback(Raz::Mouse::RIGHT_CLICK, [&compute, &render] (float) {
    compute.use();
    compute.sendUniform("uniMouseRightClick", true);

    const Raz::Vec2f mousePos = render.getWindow().recoverMousePosition();
    compute.sendUniform("uniMousePos", Raz::Vec2i(static_cast<int>(mousePos.x()), sceneHeight - static_cast<int>(mousePos.y())));
  }, Raz::Input::ALWAYS, [&compute] () {
    compute.use();
    compute.sendUniform("uniMouseRightClick", false);
  });

  render.getWindow().addMouseButtonCallback(Raz::Mouse::MIDDLE_CLICK, [&compute, &render] (float) {
    compute.use();
    compute.sendUniform("uniMouseMiddleClick", true);

    const Raz::Vec2f mousePos = render.getWindow().recoverMousePosition();
    compute.sendUniform("uniMousePos", Raz::Vec2i(static_cast<int>(mousePos.x()), sceneHeight - static_cast<int>(mousePos.y())));
  }, Raz::Input::ALWAYS, [&compute] () {
    compute.use();
    compute.sendUniform("uniMouseMiddleClick", false);
  });

  auto& displayPass = render.getRenderGraph().addNode(Raz::FragmentShader::loadFromSource(R"(
    in vec2 fragTexcoords;

    uniform sampler2D uniBuffer;

    layout(location = 0) out vec4 fragColor;

    void main() {
      fragColor = vec4(texture(uniBuffer, fragTexcoords).rgb, 1.0);
    }
  )"));

  displayPass.addParents(render.getGeometryPass());
  displayPass.addReadTexture(texture, "uniBuffer");

  app.run([&compute] (float) {
    for (int i = 0; i < 15; ++i)
      compute.execute(sceneWidth, sceneHeight);
  });
}
Clone this wiki locally