diff --git a/examples/app/CMakeLists.txt b/examples/app/CMakeLists.txt index fc10151f..f1d3cb6d 100644 --- a/examples/app/CMakeLists.txt +++ b/examples/app/CMakeLists.txt @@ -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) diff --git a/examples/app/vsgshadertoy/CMakeLists.txt b/examples/app/vsgshadertoy/CMakeLists.txt new file mode 100644 index 00000000..9736a3b5 --- /dev/null +++ b/examples/app/vsgshadertoy/CMakeLists.txt @@ -0,0 +1,9 @@ +set(SOURCES + vsgshadertoy.cpp +) + +add_executable(vsgshadertoy ${SOURCES}) + +target_link_libraries(vsgshadertoy vsg::vsg) + +install(TARGETS vsgshadertoy RUNTIME DESTINATION bin) diff --git a/examples/app/vsgshadertoy/glowmouse.shy b/examples/app/vsgshadertoy/glowmouse.shy new file mode 100644 index 00000000..ef07fed3 --- /dev/null +++ b/examples/app/vsgshadertoy/glowmouse.shy @@ -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); +} \ No newline at end of file diff --git a/examples/app/vsgshadertoy/rgb-balls.shy b/examples/app/vsgshadertoy/rgb-balls.shy new file mode 100644 index 00000000..94943929 --- /dev/null +++ b/examples/app/vsgshadertoy/rgb-balls.shy @@ -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); +} \ No newline at end of file diff --git a/examples/app/vsgshadertoy/vsgshadertoy.cpp b/examples/app/vsgshadertoy/vsgshadertoy.cpp new file mode 100644 index 00000000..871abe42 --- /dev/null +++ b/examples/app/vsgshadertoy/vsgshadertoy.cpp @@ -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 +//---------------------------------------------------------------------- + +#include +#include +#include + +struct ToyUniform { + vsg::ivec2 iResolution; + vsg::vec2 iMouse; + float iTime; + int iFrame; +}; +using ToyUniformValue = vsg::Value; + +// 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(fh)), + std::istreambuf_iterator()); + 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 createToyNode(const std::string& toyShader, + // output + vsg::ref_ptr& 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 +{ +public: + void apply(vsg::PointerEvent& pointerEvent) override + { + lastPointerEvent = &pointerEvent; + isPressed = pointerEvent.mask != vsg::BUTTON_MASK_OFF; + } + + vsg::ref_ptr 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 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(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; +}