Skip to content

3D curl noise animation

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

As the time of writing (2022-12-08, commit b3646d3), the following files give the result shown in this video:

RaZ Playground - 3D curl noise animation

Please note that this program uses compute shaders; to run it, you need to have a GPU supporting at least OpenGL 4.3 or the GL_ARB_compute_shader extension.

main.cpp

#include <RaZ/Application.hpp>
#include <RaZ/Data/Mesh.hpp>
#include <RaZ/Math/Transform.hpp>
#include <RaZ/Render/Camera.hpp>
#include <RaZ/Render/Material.hpp>
#include <RaZ/Render/MeshRenderer.hpp>
#include <RaZ/Render/RenderSystem.hpp>

using namespace Raz::Literals;
using namespace std::literals;

static constexpr int textureSize  = 128;
static constexpr int textureDepth = 128;

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

  auto& render = world.addSystem<Raz::RenderSystem>(1280, 720, "RaZ");

  if (!Raz::Renderer::checkVersion(4, 3) && !Raz::Renderer::isExtensionSupported("GL_ARB_compute_shader")) {
    throw std::runtime_error("Error: Compute is only available with an OpenGL 4.3 context or above, or with the 'GL_ARB_compute_shader' extension; "
                             "please update your graphics drivers or try on another computer");
  }

  Raz::Window& window = render.getWindow();

  Raz::Entity& camera = world.addEntityWithComponent<Raz::Transform>(Raz::Vec3f(-175.f, -200.f, -175.f));
  camera.addComponent<Raz::Camera>(window.getWidth(), window.getHeight(), Raz::Degrees(45.f), 0.1f, 100'000.f).setCameraType(Raz::CameraType::LOOK_AT);

  window.addKeyCallback(Raz::Keyboard::ESCAPE, [&app] (float) noexcept { app.quit(); });
  window.setCloseCallback([&app] () noexcept { app.quit(); });

  const auto perlinTexture = Raz::Texture3D::create(textureSize, textureSize, textureDepth, Raz::TextureColorspace::GRAY, Raz::TextureDataType::FLOAT16);
  Raz::Renderer::bindImageTexture(0, perlinTexture->getIndex(), 0, true, 0, Raz::ImageAccess::READ_WRITE, Raz::ImageInternalFormat::R16F);

  const auto curlTexture = Raz::Texture3D::create(textureSize, textureSize, textureDepth, Raz::TextureColorspace::RGBA, Raz::TextureDataType::FLOAT16);
  Raz::Renderer::bindImageTexture(1, curlTexture->getIndex(), 0, true, 0, Raz::ImageAccess::WRITE, Raz::ImageInternalFormat::RGBA16F);

  const auto particlesPos = Raz::Texture3D::create(textureSize, textureSize, textureDepth, Raz::TextureColorspace::RGBA, Raz::TextureDataType::FLOAT16);
  Raz::Renderer::bindImageTexture(2, particlesPos->getIndex(), 0, true, 0, Raz::ImageAccess::READ_WRITE, Raz::ImageInternalFormat::RGBA16F);

  Raz::ComputeShaderProgram perlinNoise(Raz::ComputeShader("perlin_noise_3d.comp"));
  perlinNoise.use();
  perlinNoise.sendUniform("uniNoiseFactor", 0.05f); // 0.1f gives an even more impressive result
  perlinNoise.execute(textureSize, textureSize, textureDepth);

  Raz::ComputeShaderProgram curlNoise(Raz::ComputeShader("curl_noise_3d.comp"));
  curlNoise.execute(textureSize, textureSize, textureDepth);

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

    layout(rgba16f, binding = 1) uniform readonly image3D uniCurlNoiseMap;
    layout(rgba16f, binding = 2) uniform image3D uniParticlesPos;

    void main() {
      ivec3 pixelCoords = ivec3(gl_GlobalInvocationID.xyz);
      ivec3 imgDims     = imageSize(uniParticlesPos);

      vec3 currentPos = imageLoad(uniParticlesPos, pixelCoords).xyz;

      // Assigning the particles' initial positions
      if (currentPos == vec3(0.0))
        currentPos = vec3(pixelCoords) - vec3(imgDims) / 2.0;

      ivec3 posCoords = ivec3(currentPos * 0.5 + vec3(imgDims) / 2.0);
      vec3 curlNoise  = imageLoad(uniCurlNoiseMap, posCoords).xyz;

      imageStore(uniParticlesPos, pixelCoords, vec4(currentPos + curlNoise * 3.0, 1.0));
    }
  )"sv));

  Raz::Mesh mesh;
  mesh.addSubmesh().getVertices().resize(textureSize * textureSize * textureDepth);

  Raz::Entity& particles = world.addEntity();
  auto& particlesTrans   = particles.addComponent<Raz::Transform>();

  Raz::RenderShaderProgram& particlesProgram = particles.addComponent<Raz::MeshRenderer>(mesh, Raz::RenderMode::POINT).getMaterials().front().getProgram();
  particlesProgram.setShaders(Raz::VertexShader::loadFromSource(R"(
    layout(std140) uniform uboCameraInfo {
      mat4 uniViewMat;
      mat4 uniInvViewMat;
      mat4 uniProjectionMat;
      mat4 uniInvProjectionMat;
      mat4 uniViewProjectionMat;
      vec3 uniCameraPos;
    };

    layout(std140) uniform uboModelInfo {
      mat4 uniModelMat;
    };

    uniform sampler3D uniParticlesPos;

    layout(location = 0) out vec3 vertColor;

    void main() {
      ivec3 imgDims = textureSize(uniParticlesPos, 0);

      ivec3 particleCoords;
      particleCoords.x = gl_VertexID % imgDims.x;
      particleCoords.y = (gl_VertexID / imgDims.x) % imgDims.y;
      particleCoords.z = (gl_VertexID / imgDims.x) / imgDims.y;

      vertColor = vec3(vec3(particleCoords) / vec3(imgDims));

      vec3 vertPos = texelFetch(uniParticlesPos, particleCoords, 0).xyz;
      gl_Position  = uniViewProjectionMat * uniModelMat * vec4(vertPos, 1.0);
    }
  )"sv), Raz::FragmentShader::loadFromSource(R"(
    layout(location = 0) in vec3 vertColor;

    layout(location = 0) out vec4 fragColor;

    void main() {
      fragColor = vec4(vertColor, 1.0);
    }
  )"sv));

  particlesProgram.clearAttributes();
  particlesProgram.clearTextures();
  particlesProgram.setTexture(particlesPos, "uniParticlesPos");

  app.run([&] (float deltaTime) {
    particlesTrans.rotate(10_deg * deltaTime, Raz::Vec3f(0.707106769f, 0.f, 0.707106769f));
    particlesUpdate.execute(textureSize, textureSize, textureDepth);
  });

  return EXIT_SUCCESS;
}
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;

layout(r16f, binding = 0) uniform writeonly image3D uniNoiseMap;
uniform float uniNoiseFactor = 0.01;
uniform int uniOctaveCount   = 1;

const int permutations[512] = int[](
  151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142,
  8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117,
  35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71,
  134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41,
  55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89,
  18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226,
  250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182,
  189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43,
  172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97,
  228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239,
  107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
  138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180,
  151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142,
  8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117,
  35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71,
  134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41,
  55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89,
  18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226,
  250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182,
  189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43,
  172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97,
  228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239,
  107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
  138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180
);

// Only 12 gradients are necessary; however, 16 are defined to avoid dividing by 12. These form a regular tetrahedron, thus no bias is introduced
const vec3 gradients3D[16] = vec3[](
  vec3(0.7071067691,  0.7071067691,           0.0), vec3(-0.7071067691,  0.7071067691,           0.0),
  vec3(0.7071067691, -0.7071067691,           0.0), vec3(-0.7071067691, -0.7071067691,           0.0),
  vec3(0.7071067691,           0.0,  0.7071067691), vec3(-0.7071067691,           0.0,  0.7071067691),
  vec3(0.7071067691,           0.0, -0.7071067691), vec3(-0.7071067691,           0.0, -0.7071067691),
  vec3(         0.0,  0.7071067691,  0.7071067691), vec3(          0.0, -0.7071067691,  0.7071067691),
  vec3(         0.0,  0.7071067691, -0.7071067691), vec3(          0.0, -0.7071067691, -0.7071067691),
  vec3(0.7071067691,  0.7071067691,           0.0), vec3(-0.7071067691,  0.7071067691,           0.0),
  vec3(         0.0, -0.7071067691,  0.7071067691), vec3(          0.0, -0.7071067691, -0.7071067691)
);

float smootherstep(float value) {
  return value * value * value * (value * (value * 6.0 - 15.0) + 10.0);
}

vec3 recoverGradient3D(int x, int y, int z) {
  return gradients3D[permutations[permutations[permutations[x] + y] + z] % gradients3D.length()];
}

float computePerlin(vec3 coords) {
  // Recovering integer coordinates on the cube

  int intX = int(coords.x);
  int intY = int(coords.y);
  int intZ = int(coords.z);

  int x0 = intX & 255;
  int y0 = intY & 255;
  int z0 = intZ & 255;

  // Recovering pseudo-random gradients at each corner of the quad
  vec3 leftBotBackGrad   = recoverGradient3D(x0,     y0,     z0    );
  vec3 leftBotFrontGrad  = recoverGradient3D(x0,     y0,     z0 + 1);
  vec3 rightBotBackGrad  = recoverGradient3D(x0 + 1, y0,     z0    );
  vec3 rightBotFrontGrad = recoverGradient3D(x0 + 1, y0,     z0 + 1);
  vec3 leftTopBackGrad   = recoverGradient3D(x0,     y0 + 1, z0    );
  vec3 leftTopFrontGrad  = recoverGradient3D(x0,     y0 + 1, z0 + 1);
  vec3 rightTopBackGrad  = recoverGradient3D(x0 + 1, y0 + 1, z0    );
  vec3 rightTopFrontGrad = recoverGradient3D(x0 + 1, y0 + 1, z0 + 1);

  // Computing the distance to the coordinates
  //     _____________
  //    /           /|
  //   /           / |
  //  /___________/ X|
  //  |           |/ |
  //  | xWeight   / zWeight
  //  |---------X |  /
  //  |         | yWeight
  //  |_________|_|/

  float xWeight = coords.x - float(intX);
  float yWeight = coords.y - float(intY);
  float zWeight = coords.z - float(intZ);

  float leftBotBackDot   = dot(vec3(xWeight,       yWeight      , zWeight      ), leftBotBackGrad);
  float leftBotFrontDot  = dot(vec3(xWeight,       yWeight      , zWeight - 1.0), leftBotFrontGrad);
  float rightBotBackDot  = dot(vec3(xWeight - 1.0, yWeight      , zWeight      ), rightBotBackGrad);
  float rightBotFrontDot = dot(vec3(xWeight - 1.0, yWeight      , zWeight - 1.0), rightBotFrontGrad);
  float leftTopBackDot   = dot(vec3(xWeight,       yWeight - 1.0, zWeight      ), leftTopBackGrad);
  float leftTopFrontDot  = dot(vec3(xWeight,       yWeight - 1.0, zWeight - 1.0), leftTopFrontGrad);
  float rightTopBackDot  = dot(vec3(xWeight - 1.0, yWeight - 1.0, zWeight      ), rightTopBackGrad);
  float rightTopFrontDot = dot(vec3(xWeight - 1.0, yWeight - 1.0, zWeight - 1.0), rightTopFrontGrad);

  float smoothX = smootherstep(xWeight);
  float smoothY = smootherstep(yWeight);
  float smoothZ = smootherstep(zWeight);

  float botBackCoeff  = mix(leftBotBackDot,  rightBotBackDot,  smoothX);
  float botFrontCoeff = mix(leftBotFrontDot, rightBotFrontDot, smoothX);
  float topBackCoeff  = mix(leftTopBackDot,  rightTopBackDot,  smoothX);
  float topFrontCoeff = mix(leftTopFrontDot, rightTopFrontDot, smoothX);

  float backCoeff  = mix(botBackCoeff,  topBackCoeff,  smoothY);
  float frontCoeff = mix(botFrontCoeff, topFrontCoeff, smoothY);

  return mix(backCoeff, frontCoeff, smoothZ);
}

float computeFbm(vec3 coords, int octaveCount) {
  float frequency = 1.0;
  float amplitude = 1.0;
  float total     = 0.0;

  for (int i = 0; i < octaveCount; ++i) {
    total += computePerlin(coords * frequency) * amplitude;

    frequency *= 2.0;
    amplitude *= 0.5;
  }

  return (total + 1.0) / 2.0;
}

void main() {
  float noise = computeFbm(vec3(gl_GlobalInvocationID.xyz) * uniNoiseFactor, uniOctaveCount);

  ivec3 pixelCoords = ivec3(gl_GlobalInvocationID.xyz);
  imageStore(uniNoiseMap, pixelCoords, vec4(vec3(noise), 1.0));
}

curl_noise_3d.comp

layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;

layout(r16f, binding = 0) uniform readonly image3D uniInputNoiseMap;
layout(rgba16f, binding = 1) uniform writeonly image3D uniOutputNoiseMap;

vec3 computeCurl(ivec3 pixelCoords) {
  float xPosDiff = imageLoad(uniInputNoiseMap, ivec3(pixelCoords.x + 1, pixelCoords.y, pixelCoords.z)).r;
  float xNegDiff = imageLoad(uniInputNoiseMap, ivec3(pixelCoords.x - 1, pixelCoords.y, pixelCoords.z)).r;
  float xGrad    = (xPosDiff - xNegDiff) * 0.5;

  float yPosDiff = imageLoad(uniInputNoiseMap, ivec3(pixelCoords.x, pixelCoords.y + 1, pixelCoords.z)).r;
  float yNegDiff = imageLoad(uniInputNoiseMap, ivec3(pixelCoords.x, pixelCoords.y - 1, pixelCoords.z)).r;
  float yGrad    = (yPosDiff - yNegDiff) * 0.5;

  float zPosDiff = imageLoad(uniInputNoiseMap, ivec3(pixelCoords.x, pixelCoords.y, pixelCoords.z + 1)).r;
  float zNegDiff = imageLoad(uniInputNoiseMap, ivec3(pixelCoords.x, pixelCoords.y, pixelCoords.z - 1)).r;
  float zGrad    = (zPosDiff - zNegDiff) * 0.5;

  return vec3(yGrad - zGrad, zGrad - xGrad, xGrad - yGrad);
}

void main() {
  ivec3 pixelCoords = ivec3(gl_GlobalInvocationID.xyz);
  vec3 noise        = computeCurl(pixelCoords);
  imageStore(uniOutputNoiseMap, pixelCoords, vec4(noise, 1.0));
}
Clone this wiki locally