Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a shadertoy example #314

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ add_subdirectory(vsganaglyphicstereo)
add_subdirectory(vsgwindows)
add_subdirectory(vsgcameras)
add_subdirectory(vsgvalidate)
add_subdirectory(vsgshadertoy)
add_subdirectory(vsgaxes)

if (vsgXchange_FOUND)
Expand Down
9 changes: 9 additions & 0 deletions examples/app/vsgshadertoy/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
set(SOURCES
vsgshadertoy.cpp
)

add_executable(vsgshadertoy ${SOURCES})

target_link_libraries(vsgshadertoy vsg::vsg)

install(TARGETS vsgshadertoy RUNTIME DESTINATION bin)
31 changes: 31 additions & 0 deletions examples/app/vsgshadertoy/glowmouse.shy
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Draw a glowing spot around the mouse position

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized coordinates (from 0 to 1)
vec2 uv = fragCoord / iResolution.xy;

// Correct uv using the aspect ratio to ensure the circle's aspect ratio is maintained
float aspectRatio = 1.0*iResolution.x / iResolution.y;
vec2 correctedUV = vec2(uv.x * aspectRatio, uv.y);

// Background gradient: black at the bottom, dark blue at the top
vec3 bgColor = mix(vec3(0.0, 0.0, 0.0), vec3(0.0, 0.0, 0.4), uv.y);

// Mouse position in normalized coordinates, corrected for aspect ratio
vec2 mousePos = iMouse.xy / iResolution.xy;
vec2 correctedMousePos = vec2(mousePos.x * aspectRatio, mousePos.y);

// Calculate distance from current fragment to mouse position, corrected for aspect ratio
float distanceToMouse = distance(correctedUV, correctedMousePos);

// Glow effect based on distance to mouse position
float glowIntensity = 0.1 / sqrt(distanceToMouse);
vec3 glowColor = vec3(1.0, 0.0, 0.0) * glowIntensity;

// Combine background and glow color
vec3 color = bgColor + glowColor;

// Output the color
fragColor = vec4(color, 1.0);
}
37 changes: 37 additions & 0 deletions examples/app/vsgshadertoy/rgb-balls.shy
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Correct aspect ratio
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;

// Background gradient
vec3 col = mix(vec3(0.0, 0.0, 0.0), vec3(0.0, 0.0, 0.4), uv.y * 0.5 + 0.5);

// Light source direction
vec3 lightDir = normalize(vec3(-10.0, -10.0, 20.0));

// Sphere properties
float sphereRadius = 0.20;
vec3 sphereColors[3] = vec3[](vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0));

// Time factor for rotation
float time = iTime * 0.5;

// Draw each sphere
for(int i = 0; i < 3; i++) {
float angle = time + float(i) * 2.0944; // 120 degrees apart
vec2 position = vec2(sin(angle), cos(angle)) * sphereRadius * 2;

// Sphere shading
float dist = length(uv - position);
if(dist < sphereRadius) {
float diff = dot(lightDir, normalize(vec3(position - uv, sqrt(sphereRadius * sphereRadius - dist * dist))));
diff = clamp(diff, 0.0, 1.0);

// Combine sphere color with light intensity and background
col = mix(col, sphereColors[i] * diff, smoothstep(sphereRadius, sphereRadius - 0.03, dist));
}
}

// Output to screen
fragColor = vec4(col,1.0);
}
293 changes: 293 additions & 0 deletions examples/app/vsgshadertoy/vsgshadertoy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
//======================================================================
// This programs allows playing with the fragment shader. The syntax
// of the shader programs is based on the shadertoy syntax at
// https://www.shadertoy.com/new. Note that currently only
// the following uniforms are supported:
//
// uniform vec2 iResolution; // viewport resolution (in pixels)
// uniform float iTime; // shader playback time (in seconds)
// uniform vec2 iMouse; // mouse pixel coords. xy: current
// uniform int iFrame; // shader playback frame
//
// However it is enough to run a large number of shadertoy shaders.
//
// 2024-07-20 Sat
// Dov Grobgeld <[email protected]>
//----------------------------------------------------------------------

#include <iostream>
#include <vsg/all.h>
#include <chrono>

struct ToyUniform {
vsg::ivec2 iResolution;
vsg::vec2 iMouse;
float iTime;
int iFrame;
};
using ToyUniformValue = vsg::Value<ToyUniform>;

// The fixed toy vertex shader
const auto shadertoy_vert = R"(
#version 450

layout(set = 0, binding = 0) uniform UBO {
ivec2 iResolution;
vec2 iMouse;
float iTime;
int iFrame;
} ubo;

layout(location = 0) out vec2 fragCoord;

out gl_PerVertex{ vec4 gl_Position; };

void main()
{
// fragCord is from (0→w,0→h)
fragCoord = vec2((gl_VertexIndex << 1) & 2,
(gl_VertexIndex & 2)) * ubo.iResolution;

// gl_Position is from (-1→1,-1→1)
gl_Position = vec4(fragCoord.x/ubo.iResolution.x * 2.0 - 1.0,
(1.0-fragCoord.y/ubo.iResolution.y) * 2.0 - 1.0,
0.5, 1.0);
}
)";


std::string readFile(const vsg::Path& filename)
{
std::ifstream fh(filename);

if (!fh.good())
throw std::runtime_error(std::string("Error opening file \"") + filename + "\" for input!");

std::string ret;
ret.assign((std::istreambuf_iterator<char>(fh)),
std::istreambuf_iterator<char>());
fh.close();
return ret;
}

const std::string defaultShader = R"(
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized coordinates
vec2 uv = fragCoord/iResolution.xy;

// Time varying pixel color
vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));

// Output to screen
fragColor = vec4(col,1.0);
}
)";

std::string shaderToyToFragmentShader(const std::string& toyShader)
{
return R"(
#version 450

layout(set = 0, binding = 0) uniform UBO {
ivec2 iResolution;
vec2 iMouse;
float iTime;
int iFrame;
} ubo;

layout(location = 0) in vec2 fragCoord;
layout(location = 0) out vec4 fragColor;

// Create shortcuts to the uniform buffer. This could a
// be solved by a search and replace of the shader string.
ivec2 iResolution = ubo.iResolution;
float iTime = ubo.iTime;

vec2 iMouse = ubo.iMouse;
int iFrame = ubo.iFrame;

// This should be exactly shadertoy syntax
)" + toyShader + R"(
void main()
{
mainImage(fragColor, fragCoord);
})";

}


// Create a vsg node containing the shadertoy command
vsg::ref_ptr<vsg::Node> createToyNode(const std::string& toyShader,
// output
vsg::ref_ptr<ToyUniformValue>& toyUniform)
{
// load shaders
auto vertexShader = vsg::ShaderStage::create(VK_SHADER_STAGE_VERTEX_BIT, "main", shadertoy_vert);

auto fragmentShader = vsg::ShaderStage::create(VK_SHADER_STAGE_FRAGMENT_BIT, "main", shaderToyToFragmentShader(toyShader));

if (!vertexShader || !fragmentShader)
throw std::runtime_error("Could not create shaders.");

toyUniform = ToyUniformValue::create();
toyUniform->properties.dataVariance = vsg::DataVariance::DYNAMIC_DATA;
toyUniform->value().iResolution = {800,600};
toyUniform->value().iTime = 0;
toyUniform->value().iFrame = 0;
toyUniform->value().iMouse = vsg::vec2 {0,0};
toyUniform->dirty();

// set up graphics pipeline
vsg::DescriptorSetLayoutBindings descriptorBindings{
{0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT|VK_SHADER_STAGE_FRAGMENT_BIT, nullptr} // { binding, descriptorType, descriptorCount, stageFlags, pImmutableSamplers}
};

auto descriptorSetLayout = vsg::DescriptorSetLayout::create(descriptorBindings);

vsg::GraphicsPipelineStates pipelineStates{
vsg::VertexInputState::create(), // No vertices for shader toy
vsg::InputAssemblyState::create(),
vsg::RasterizationState::create(),
vsg::MultisampleState::create(),
vsg::ColorBlendState::create(),
vsg::DepthStencilState::create()};

auto toyUniformDescriptor = vsg::DescriptorBuffer::create(toyUniform, 0, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);

auto pipelineLayout = vsg::PipelineLayout::create(vsg::DescriptorSetLayouts{descriptorSetLayout}, vsg::PushConstantRanges {});
auto graphicsPipeline = vsg::GraphicsPipeline::create(pipelineLayout, vsg::ShaderStages{vertexShader, fragmentShader}, pipelineStates);
auto bindGraphicsPipeline = vsg::BindGraphicsPipeline::create(graphicsPipeline);

auto descriptorSet = vsg::DescriptorSet::create(descriptorSetLayout, vsg::Descriptors{toyUniformDescriptor});
auto bindDescriptorSet = vsg::BindDescriptorSet::create(VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, descriptorSet);

// create StateGroup as the root of the scene/command graph to hold the GraphicsPipeline, and binding of Descriptors to decorate the whole graph
auto node = vsg::StateGroup::create();
node->add(bindGraphicsPipeline);
node->add(bindDescriptorSet);

// setup geometry
auto drawCommands = vsg::Commands::create();
drawCommands->addChild(vsg::Draw::create(3, 1, 0, 0)); // Draw without vertices, as they are generated from gl_VertexIndex

// add drawCommands to transform
node->addChild(drawCommands);

return node;
}

class MouseHandler : public vsg::Inherit<vsg::Visitor, MouseHandler>
{
public:
void apply(vsg::PointerEvent& pointerEvent) override
{
lastPointerEvent = &pointerEvent;
isPressed = pointerEvent.mask != vsg::BUTTON_MASK_OFF;
}

vsg::ref_ptr<vsg::PointerEvent> lastPointerEvent;
bool isPressed = false;
};

int main(int argc, char** argv)
{
// set up defaults and read command line arguments to override them
vsg::CommandLine arguments(&argc, argv);

auto options = vsg::Options::create();
options->fileCache = vsg::getEnv("VSG_FILE_CACHE");
options->paths = vsg::getEnvPaths("VSG_FILE_PATH");

auto windowTraits = vsg::WindowTraits::create();
windowTraits->debugLayer = arguments.read({"--debug", "-d"});
windowTraits->apiDumpLayer = arguments.read({"--api", "-a"});
arguments.read({"--window", "-w"}, windowTraits->width, windowTraits->height);

if (arguments.errors()) return arguments.writeErrorMessages(std::cerr);


windowTraits->height = 600;
windowTraits->width = 800;

auto scene = vsg::Group::create();

// Read the shader from the command line
std::string toyShader;
if (argc < 2)
{
windowTraits->windowTitle = "vsgshadertoy";
toyShader = defaultShader;
}
else
{
vsg::Path filePath(argv[1]);
toyShader = readFile(filePath);

windowTraits->windowTitle = std::string("vsgshadertoy: ") + vsg::simpleFilename(filePath) + vsg::fileExtension(filePath);
}


// Add the image to the scene
vsg::ref_ptr<ToyUniformValue> toyUniform;
scene->addChild(createToyNode(toyShader,
// output
toyUniform
));
toyUniform->dirty();

// create the viewer and assign window(s) to it
auto viewer = vsg::Viewer::create();
auto window = vsg::Window::create(windowTraits);
if (!window)
{
std::cout << "Could not create window." << std::endl;
return 1;
}

viewer->addWindow(window);

// camera related details
auto viewport = vsg::ViewportState::create(0, 0, window->extent2D().width, window->extent2D().height);
auto ortho = vsg::Orthographic::create();
ortho->nearDistance = -1000;
ortho->farDistance = 1000;
auto lookAt = vsg::LookAt::create(vsg::dvec3(0, 0, 1.0), vsg::dvec3(0.0, 0.0, 0.0), vsg::dvec3(0.0, 1.0, 0.0));
auto camera = vsg::Camera::create(ortho, lookAt, viewport);

auto commandGraph = vsg::createCommandGraphForView(window, camera, scene);
viewer->assignRecordAndSubmitTaskAndPresentation({commandGraph});

// compile the Vulkan objects
viewer->compile();

// assign a CloseHandler to the Viewer to respond to pressing Escape or the window close button
viewer->addEventHandlers({vsg::CloseHandler::create(viewer)});

// Handle the mouse and resize
auto mouseHandler = MouseHandler::create();
viewer->addEventHandler(mouseHandler);

// main frame loop
auto t0 = std::chrono::steady_clock::now();
while (viewer->advanceToNextFrame())
{
auto extent = window->extent2D();
toyUniform->value().iResolution = {(int)extent.width, (int)extent.height};
toyUniform->value().iTime = std::chrono::duration<float>(std::chrono::steady_clock::now()-t0).count();
toyUniform->value().iFrame += 1;

if (mouseHandler->isPressed)
toyUniform->value().iMouse = mouseHandler->lastPointerEvent ? vsg::vec2(mouseHandler->lastPointerEvent->x, extent.height-mouseHandler->lastPointerEvent->y) : vsg::vec2(0,0);

toyUniform->dirty();

viewer->handleEvents();
viewer->update();
viewer->recordAndSubmit();
viewer->present();
}

// clean up done automatically thanks to ref_ptr<>
return 0;
}