Rayground is a web-based framework for rapid prototyping of algorithms based on the ray tracing paradigm. Its main goal is to help develop, test and share self contained modules that showcase a particular method or technique. This work aims to demystify ray tracing fundamentals while relying on the established GLSL shading language for code development and the underlying WebGL pipeline for its hidden execution model. It is intended for people who are already familiar with the basics of computer graphics theory and shader-based programming. A detailed documentation of the Rayground's programming interface is available on-site, coupled with many demo and tutorial projects, in order tο ease the way of newcomers.
Figure 1. A collection of ray traced projects created on the web using Rayground.
Rayground is open, cross-platform, and available to everyone. It does not rely on any browser plugins and thus runs on any platform that has a modern standards compliant browser. Specifically, the website runs only on WebGL2-enabled browsers and requires the EXT_color_buffer_float
extension. A prototype version of WebRays was successfully utilized for the implementation of the Rayground platform. WebRays
exposes an easy-to-use and explicit API with lightweight support for acceleration data structures, to enable ray/triangle intersection functionality in Web-based applications.
The goals of this github account are:
- Guide newcomers with a step-by-step navigation to the Rayground' API through a simple path tracing implementation.
- Inform the active users what new features are coming in the next version(s) of the website.
- Offer accessibility for bug reporting, enhancements or feature requests.
- Provide a brief summary our sponsors.
Rayground aims to demystify raytracing fundamentals, by providing a well-defined WebGL-based programmable graphics pipeline of configurable distinct raytracing stages coupled with a simple scene description format. The graphical user interface of Rayground is designed to have two discrete parts, the preview window and the shader editor, similar to the layout of ShaderToy, which many shader developers are already familiar with (Fig. 2). Visual feedback is interactively provided in the WebGL rendering context of the preview canvas, while the user performs live source code modifications. Rayground follows a programmable GPU-accelerated ray-tracing pipeline in order to give developers direct and flexible control of five ray tracing stages through a simple, high-level shader-based programming model. Thus, the shader editor consists of five tabs, corresponding to five customisable shader stage, namely Scene, Generate, Hit, Miss and PostProcess.
Figure 2. (Left) The Rayground interface, with the preview window and the shader editor, showing the Generate stage. (Middle and Right) Hit and Miss event shaders for the same project, where the Cornell Box scene is rendered using a simple path tracer. Complete implementation details can be found here.
At the core of Rayground there is a traditional ray tracing image synthesis pipeline, with several programmable stages via event handling shaders. It was designed with the aim to help users gradually understand how a ray tracer works, without getting distracted by the particular implementation of the framework or platform-specific characteristics. Since ray tracing is now tightly integrated into modern real-time rendering APIs (Optix, DX12, Vulkan), we follow a similar programming model. The geometric objects of the scene are initially specified in the input Scene declaration stage. These objects are used to build ray intersection acceleration data structures, which in our case, are not programmable. Primary rays, which, in the simplest case correspond to a virtual camera, are generated and submitted for intersection in the Generate stage. Depending on the intersection results, execution switches to the closest Hit or Miss stage. Both events can generate a new ray which, in turn, may be intersected with the scene to trigger new events. Users are provided with several built-in and user-controlled properties that ease the data transmission between events. For each iteration of the pipeline, or frame, a pixel colour is computed and blended with the previous values stored in an Accumulation Buffer. All code segments execute in parallel for each pixel of the Canvas, i.e. the preview window and, in every frame, the executed code directly corresponds to the one iteration event, i.e. one ray path. The intermediate image is finally filtered through a PostProcess stage, a common step prior to image presentation, handy for tone mapping and filtering operations. A graphical illustration of the pipeline is shown in the following figure.
Figure 3. High level overview of the Rayground pipeline.
The Rayground API is implemented using the WebGL2 standard, supporting shader programming via GLSL, thus providing a convenient and familiar code development interface. The user is encouraged to use built-in GLSL functions (e.g. dot, cross) and types (e.g. vec4, mat4). However, any use of the standard input and output variables of the GLSL programmable pipeline stages (e.g. gl_FragCoord) as well as samplers (e.g. sampler2D) may result in undefined behaviour and should be avoided. While certain functionality is common to all stages, there are also stage-specific input and output variables, which are described below in more detail. The basic functionality of Rayground API is listed in the following table. A detailed explanation of the Rayground's programming interface can be found in the documentation section of Rayground site.
Type | Name | Description |
---|---|---|
All stages | ||
in vec2 |
rg_Canvas |
canvas resolution in pixels |
in vec2 |
rg_Pixel |
pixel coordinates relative to the lower left corner of the canvas |
in int |
rg_Frame |
current frame counter |
in float |
rg_Time |
time in seconds since simulation start, updates in every frame |
in uvec4 |
rg_Seed |
cpu generated seed values, updated for each stage |
in float |
RG_RAY_MAX_DISTANCE |
1.e27 |
in float |
RG_RAY_ACTIVE_FLAG |
1.0 |
in float |
RG_RAY_INACTIVE_FLAG |
0.0 |
in float |
RG_PI |
3.14159265359 |
in vec4 |
rg_Random(uint i, uint s0, uint s1) |
Obtain 4 random values from a counter-based pseudo-random sequence |
Generate/Hit/Miss stages | ||
out vec4 |
rg_RayOrigin |
output ray origin (.xyz position, .w active flag) |
out vec4 |
rg_RayDirection |
output ray direction (.xyz direction, .w max travel distance) |
out vec4 |
rg_Payload0..3 |
output ray payload values |
out vec4 |
rg_Accumulation |
output ray accumulation color (.rgb values, .a additive blending factor) |
in vec4 |
rg_PrevRayOrigin |
input ray origin |
in vec4 |
rg_PrevRayDirection |
input ray direction |
in vec4 |
rg_PrevPayload0..3 |
input ray payload |
in vec4 |
rg_PrevAccumulation |
input ray accumulation color |
in int |
rg_Depth |
ray depth iteration, starts at 0 and gets incremented after each intersection |
bool |
rg_TraceOcclusion(vec3 o, vec3 d, float t) |
occlusion query along a specified ray segment |
Generate stage | ||
void |
rg_generate(void) |
entry point signature |
Hit stage | ||
void |
rg_hit(void) |
entry point signature |
in vec3 |
rg_Hitpoint |
hit in world space coordinates |
in vec3 |
rg_Normal |
geometric normal of the intersected primitive |
in int |
rg_MaterialID |
material ID of the intersected primitive |
vec4 |
rg_MaterialPropertyI(int id) |
material properties as specified in the scene stage, where I=[0,7] |
Miss stage | ||
void |
rg_miss(void) |
entry point signature |
PostProcess stage | ||
void |
rg_post_process(void) |
entry point signature |
vec4 |
rg_PixelColor |
final pixel color to be presented |
rg_Image2D |
rg_AccumulatedImage |
2D image buffer with the accumulated values (after blending operation) |
vec4 |
rg_ImageFetch2D(rg_Image2D I, ivec2 uv) |
2D image fetch from the specified pixel coordinates |
We provide here a basic example, in order to present the pipeline and demonstrate Rayground’s API mechanisms. It implements a simple unidirectional path tracer for diffuse surfaces in only a few lines of code. For clarity, the code of certain functions, commonly encountered in a typical path tracer, has been omitted (function declarations are included in the code listings below). Figures 2 (left) and 4 illustrate how the Cornell Box scene is illuminated using this code after 500 samples per pixel. For more details, the interested reader can find the complete working example here.
The Cornell Box scene is trivially represented using five quads and two cubes for the diffuse geometry. The material properties are configured accordingly to set the colour for diffuse surfaces. The maximum iteration depth of the scene is set to five since additional bounces will add negligible radiance contribution.
After the scene description, this stage is used to implement a simple pinhole perspective camera. The rg_Pixel and rg_Canvas attributes are used to calculate a ray going through the centre of each pixel for a perspective camera with a 45 field of view. The generated rays are then submitted for intersection after preparing their rg_RayOrigin and rg_RayDirection variables. Additionally, the rg_Accumulation and rg_Payload0 values are also initialised for each generated ray at this step.
vec3 get_camera_ray_dir(vec3 eye, vec3 target, float aperture, bool jitter);
void rg_generate() {
/* Set Camera Parameters */
vec3 eye = vec3(0.0, 0.5, 3.0);
vec3 target = vec3(0.0, 0.5, 0.0);
float fov = 45.0;
bool jitter = true;
/* Generate a Ray based on a Pinhole Perspective Camera */
vec3 dir = get_camera_ray_dir(eye, target, fov, jitter, rg_Pixel, rg_Canvas);
/* Submit Primary Ray */
rg_RayOrigin = vec4(eye, RG_RAY_ACTIVE_FLAG);
rg_RayDirection = vec4(dir, RG_RAY_MAX_DISTANCE);
/* Initialize Accumulation and Payload */
rg_Accumulation = vec4(0.0, 0.0, 0.0, 1.0/float(rg_Frame));
rg_Payload0 = vec4(1.0);
}
In a following fixed stage, the intersection of the primary rays with the scene entities is performed by the core ray tracing engine and the corresponding events for each event (hit or miss) are subsequently triggered.
This stage, called upon valid ray-object intersections, demonstrates the radiance accumulation process for the diffuse Monte Carlo path tracer using next event estimation; the direct illumination of each ray path is estimated by testing the intersected point for visibility with a punctual light source, using a shadow ray, and accounting for its contribution to the incident radiance. The important steps at this stage are the computation of the incoming illumination at the intersected point, the generation of a new ray and the storage of the light throughput at the current ray path. These computations can be trivially accomplished using the provided Rayground variables. For example, information about the intersected point can be obtained using rg_Hitpoint, rg_Normal and rg_MaterialProperty. To check for visibility, the rg_TraceOcclusion() casts a shadow ray between the intersection point and the light source position. Two final operations are necessary to complete the shader. First, new rays are submitted for intersection by writing the appropriate values to rg_RayOrigin and rg_RayDirection and making sure the RG_ACTIVE_FLAG is set in the fourth coordinate of rg_RayOrigin. Second, the rg_Accumulation and rg_Payload0 variables are updated to store the gathered radiance and the accumulated light throughput, recursively.
float get_pdf (vec3 n, vec3 d);
float get_ndotl (vec3 n, vec3 wi);
vec3 get_diffuse_bsdf (vec3 c);
vec3 get_new_ray_direction(vec3 n);
void rg_hit() {
/* Set Light Parameters */
vec3 light_pos = vec3(0,0.999,0);
vec3 light_intensity = vec3(1.5);
/* Connect Hit Point to Light Source */
float ray_offset = 0.001;
vec3 dir_to_light = normalize(light_pos - rg_Hitpoint);
float dist = distance (light_pos, rg_Hitpoint);
vec3 shadow_ray = rg_Hitpoint + ray_offset*dir_to_light;
/* Send Shadow Ray */
float dist_bias = dist - ray_offset*2.0;
float visibility = rg_TraceOcclusion(shadow_ray, dir_to_light, dist_bias) ? 0.0 : 1.0;
/* Get the NdotL Factor */
float NdotL = get_ndotl(rg_Normal, dir_to_light);
/* Get the Material Value */
vec3 diffuse_mat = rg_MaterialProperty0(rg_MaterialID).rgb;
vec3 bsdf = get_diffuse_bsdf(diffuse_mat);
/* Compute the New Colour Value */
vec3 Li = light_intensity*visibility;
Li /= (dist*dist+0.001);
vec3 radiance = Li*bsdf*NdotL;
/* Create the New Ray */
vec3 pos = rg_Hitpoint + ray_offset*rg_Normal;
vec3 dir = get_new_ray_direction(rg_Normal);
rg_RayOrigin = vec4(pos, RG_RAY_ACTIVE_FLAG);
rg_RayDirection = vec4(dir, RG_RAY_MAX_DISTANCE);
/* Update Colour and Throughput Values */
rg_Accumulation = rg_PrevAccumulation;
rg_Accumulation.rgb += rg_PrevPayload0.rgb*radiance.rgb;
float pdf = get_pdf(rg_Normal, dir);
float NdotI = max(0.0, dot(rg_Normal, dir));
vec3 throughput = bsdf*NdotI/pdf;
rg_Payload0 = rg_PrevPayload0;
rg_Payload0.rgb *= throughput.rgb;
}
This stage is called when an intersection with the scene is not found. In this example, the shader simply terminates the ray shooting and sets the incoming colour value to the rg_Accumulation buffer.
void rg_miss() {
/* Terminate Ray Shooting */
rg_RayOrigin = vec4(0.0, 0.0, 0.0, RG_RAY_INACTIVE_FLAG);
rg_RayDirection = vec4(0.0);
/* Set Accumulation Colour and Blending Factor */
rg_Accumulation = rg_PrevAccumulation;
}
Finally, this stage applies a basic gamma correction filter to each cell of the accumulated buffer and assigns the resulting values to the corresponding output canvas pixel through rg_PixelColor.
vec4 gamma_correction(vec4 color, float gamma);
void rg_post_process() {
/* Get Accumulation Buffer */
vec4 image = rg_ImageFetch2D(rg_AccumulatedImage, ivec2(rg_Pixel));
/* Perform Gamma Correction */
rg_PixelColor = gamma_correction(image, 2.2));
}
Figure 4. Unidirectional diffuse path tracer.
Motivation. The advent of mass-produced, consumer grade hardware with raytracing acceleration capabilities has significantly boosted the interest of the graphics community and has led to the introduction of related methods to interactive applications, thus demonstrating its wide applicability to students. Unfortunately, this turn of interest to ray-tracing-based techniques is not sufficiently backed by proper educational tools to assist students in becoming familiar with the basic concepts and help them become practically engaged in building their own projects. Moreover, modern low-level graphics APIs, either dedicated to ray tracing like NVIDIA OptiX or supporting it, such as Microsoft DirectX 12 and Khronos Group Vulkan, pose high entry barriers to students and require a very daunting and long learning process, riddled with many distracting technicalities.
Idea. In [VGV+20], Rayground is used as an online, interactive education tool for richer in-class teaching and gradual self-study by providing a convenient introduction into practical ray tracing. In this work, an extensive discussion is offered describing how both undergraduate (Fig. 5) and postgraduate (Fig. 6) computer graphics theoretical lectures and laboratory sessions can be enhanced by Rayground, in order to achieve a broad understanding of the underlying concepts. For more details please refer to the original manuscript provided below.
Resources. To encourage more teachers to incorporate Rayground into their computer graphics and visualization courses (see [VGV+21]), the teaching material built during our course to support remote learning as well as to promote auto-didactic engagement and collaboration (such as presentations and lab exercises) are provided freely from here. Last but not least, details and online demos about WebRays
[VGV+21_2] - a GPU-accelerated ray intersection engine for the World Wide Web - which has been deployed at the core of the Rayground platform are available here.
[VGV+20] Vitsas N., Gkaravelis A., Vasilakis A. A., Vardis K., Papaioannou G., 'Rayground: An Online Educational Tool for Ray Tracing'. In Eurographics 2020 - Education Papers (2020), The Eurographics Association.
[VGV+21] Vasilakis A. A., Papaioannou G., Vitsas N., Gkaravelis A., 'Remote Teaching Advanced Rendering Topics using the Rayground Platform'. In IEEE Computer Graphics & Applications, vol. 41 , issue 5, 99-103, Sep, 2021.
[VGV+21_2] Vitsas N., Gkaravelis A., Vasilakis A. A., Papaioannou G., 'WebRays: Ray Tracing on the Web'. In Ray Tracing Gems II, ch. 18, August, 2021.
Figures.
Figure 5. Output of the first (a-c) and second (d) undergraduate lab sessions. a) Unlit shading of supported primitives. b) Cornell Box using normal vector colouring and c) Lambertian shading. d) Whitted ray tracing.
Figure 6. Output of the graduate lab session tasks. a) Ambient occlusion. b) Unidirectional path tracer using importance sampling. c) Comparison of BRDF versus light importance sampling. d) Volumetric rendering.
A. rg_obj2vertices.py: Transforms an .obj
file into a raw vertex buffer of a triangular mesh that can be used as an array of vertices
for a triangles
object type in Scene description. Usage:
python rg_obj2vertices.py <obj_file.obj> <output_file.txt>
B. rg_upload_project.py: Uploads a saved Rayground project, extracted in .json
form, into a newly created project. Run 'output_code.txt' in console (F12) and then reload web page (F5) to refresh project contents. Usage:
python rg_upload_project.py <json_file.json> <output_code.txt>
Rayground would not have been possible without these amazing open-source technologies: