Convert Shadertoy shaders to Lens Studio!
First, read the official Shadertoy to Lens Studio Guide and watch Michael Porter's video.
This repository includes all the shaders from Micheal Porter's video, recreated in Lens Studio 5.0. All code in this guide is is in this repo.
Paste shader code from Shadertoy into the above link, and paste it into a Code Node in Lens Studio. I have forked and added upon Hart Woolery's original conversion tool to handle more cases.
In this Shadertoy of a blue circle at half the height of the screen, our main function looks like this.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
fragColor = vec4(uv, 0, 1); // Draw red-green gradient
// Blue circle of radius 1, at 0,0.
if (abs(length(uv) - 1.0) < 0.04) {
fragColor = vec4(0, 0, 1, 1);
}
}
To set that up for Lens Studio, see "1.1 - Circle - Using resolution" in this project.
- Make a new Screen Image. In the Inspector Panel for the Screen Image, set
Stretch Mode
toStretch
.- Optionally, for performance and if your shader don't need screen resolution and is a square, set
Stretch Mode
toFill
and skip setup from step 4 and on.
- Optionally, for performance and if your shader don't need screen resolution and is a square, set
- Select the Screen Image > Select Material > Select the Shader Graph. Setup the Shader Graph like so.
- Convert the Shadertoy code into Code Node code using Shadertoy to Code Node (Improved!). In the Shader Graph in Lens Studio, paste that code into the Code Node.
// The shader output color
output_vec4 fragColor;
// The shader resolution
input_vec2 resolution;
vec3 getResolution() { if (resolution.x == 0.) return vec3(640,640,1); return vec3(resolution,1);}
#define iResolution getResolution()
void main()
{
vec2 fragCoord = system.getSurfaceUVCoord0() * iResolution.xy;
// Nomalized to [0, 1] on the x-axis.
vec2 uv = (2.*fragCoord - iResolution.xy)/iResolution.x;
// Red-green gradient
fragColor = vec4(uv, 0, 1);
// Blue circle of radius 1, at 0,0.
if (abs(length(uv) - 1.0) < 0.04) {
fragColor = vec4(0, 0, 1, 1);
}
}
- In the Inspector Panel for the Material, drag
Device Camera Texture
from the Assets Panel as theResolution Texture
. SetFiltering Mode
toNearest
.
In Lens Studio, textureSize()
only works when the texture sample contributes to the output. If we don't need to sample the texture, we can add the following in the main() function of our Code Node.
This allows us to simplify the Shader Graph from step 2 above this.
// Texture used to get the resolution
input_texture_2d resolutionTexture;
void main() {
//...
// Add this to only sample one pixel on the first frame.
if (system.getTimeElapsed() == 0.0 && system.getSurfaceUVCoord0().y == 1.0 && resolutionTexture.textureSize().x == 0.0) {
fragColor = mix(resolutionTexture.sample(vec2(0)), fragColor, 0.9999999);
}
}
See "1.3 - Circle - Using resolution (fewest nodes)" example in this project.
Pixel coordinates, fragCoord,, uv, and resolution work exactly the same in Shadertoy and Lens Studio.
Ranges are the same in Shadertoy and LS.
Variable | Shadertoy | Lens Studio | Range |
---|---|---|---|
vec2 pixelCoord |
vec2(floor(uv * iResolution.xy)) |
vec2(floor(uv * iResolution.xy)) |
[0, resolution - 1] |
vec2 uv |
fragCoord/iResolution.xy or (pixelCoords + 0.5) / iResolution.xy |
system.getSurfaceUVCoord0() or (pixelCoords + 0.5) / iResolution.xy |
[0, 1] |
vec2 fragCoord |
fragCoord |
uv * iResolution.xy |
[0.5, iResolution.x - 0.5] |
vec3 iResolution |
iResolution |
input_texture_2d inputTexture; vec3(inputTexture.textureSize(), 1.0) |
Constant whole number depending on screen size |
Examples for the x-axis for resolution of 4 and 3. The value below the arrow ^
is the position on the horizonal number line.
resolution = 4 | resolution = 3
_________________ | _____________
pixelCoord | 0 | 1 | 2 | 3 | | pixelCoord | 0 | 1 | 2 |
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | ‾‾‾‾‾‾‾‾‾‾‾‾‾
^ ^ ^ ^ ^ | ^ ^ ^ ^
position 0 1 2 3 4 | position 0 1 2 3
|
^ ^ ^ ^ | ^ ^ ^
fragCoord 0.5 1.5 2.5 3.5 | fragCoord 0.5 1.5 2.5
|
^ ^ ^ ^ | ^ ^ ^
uv 1/8 3/8 5/8 7/8 | uv 1/6 3/6 5/6
Here are two useful functions to convert between uv and pixel coordinates.
vec2 uvToPixelCoords(vec2 uv) {
return vec2(floor(uv * iResolution.xy));
}
vec2 pixelCoordsToUV(vec2 pixelCoords) {
return (pixelCoords + 0.5) / iResolution.xy;
}
In Shadertoy, we use iChannel0
/iChannel1
/iChannel2
/iChannel3
to insert 2D textures, such as images, videos, or 2D buffers. We use sample
or sampleLod
as above to read the color at a position. In Shadertoy, we can pass data from one frame to the next by encoding data as a color on a specific pixel, and reading that pixel on the next frame.
For the full code, see Shadertoy Example and "3.4 - Read and write to pixel" for the Lens Studio equivalent.
Write to pixel in Shadertoy or Lens Studio:
// Step1: Draw an aqua pixel at (30, 30)
vec2 pixelCoord = uvToPixelCoords(uv);
if (pixelCoord.x == POS.x && pixelCoord.y == POS.y) {
fragColor = vec4(0.1, 0.6, 0.4, 0.3);
}
To read from a pixel in Shadertoy we use texture
, textureLod
or textureFetch
.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uvToRead = pixelCoordsToUV(vec2(30.0, 30.0));
// Read the pixel at (30, 30). These are equivalent.
vec4 pixelColor = texture(iChannel0, uvToRead);
vec4 pixelColor = textureLod(iChannel0, uvToRead, 0.0);
vec4 pixelColor = texelFetch(iChannel0, ivec2(30,30), 0);
To read from a pixel in Lens Studio we use sample
or sampleLod
.
input_texture_2d iChannel0;
void main() {
vec2 uvToRead = pixelCoordsToUV(vec2(30.0, 30.0));
// Read the pixel at (30, 30). These are equivalent.
vec4 pixelColor = iChannel0.sample(uvToRead);
vec4 pixelColor = iChannel0.sampleLod(uvToRead, 0.0);
In Shadertoy we may want to write to one buffer, and then use that same buffer in the next frame.
In this project, see my conversion of this shadertoy in "3.4 - Read and write to pixel" for the most basic example.
We know the Shadertoy is writing to and from the same channel, because in the code for "Buffer A"/iChannel0
, we see that we're reading from iChannel0
.
vec4 pixelColor = texture(iChannel0, uvToRead);
To set this up in Lens Studio:
-
Create a new Render Target, and name it
Feedback Render Target
, and set it to get its texture from the Render Target your shader is outputting to. -
Create a new empty Orthographic Camera, set it to a different layer (e.g. "feedback a"). Set it to output to
Feedback Render Target
. -
In the Code Node, we declare
iChannel0
.
input_texture_2d iChannel0;
-
In the Shader Graph, we add a
Texture 2D Object Parameter
node, and set it as the input ofiChannel0
in the Code Node, and set the Title toFeedback Buffer
. -
In the Material for the shader, we set
Feedback Buffer
toFeedback Render Target
.
- Important buffer size difference:
- Shadertoy buffers has four channels of 32-bit floats, for a total of 128 bits per pixel.
- Lens Studio has only four channels of 8-bit floats between 0 and 1, for a total of 32 bits per pixel. i.e. In Lens Studio, each 8 bit channel only take on 256 different values, from 0.0/255.0, 1.0/255.0, 2.0/255.0 ... 255.0/255.0.
- If we need to precisely save all 32 bits of a float, in Lens Studio we can pack that into 4 pixels. Set min and max for values specific to your shader.
// Pack a 32-bit signed float into a Lens Studio rgba color. vec4 system.pack32Bit( float value, float min, float max ) // Unpack a Lens Studio rbga color into a 32-bit signed float. float system.unpack32Bit( vec4 value, float min, float max )
- In order to save data on the alpha channel in the output color in Lens Studio, in the Asset Browser, click the Render Target that the shader outputs to, then in the Inspector Panel,
- Set the
Clear Color Option
toNone
. If you don't do this, the alpha channel read withsample
orsampleLod
will always be 1.0. - Set
Filtering Mode
toNearest
so sampling will get the original color of a pixel instead of averaging colors from neighbouring pixels.
- Set the
In Lens Studio, add the following. This guarentees that iFrame is a value that increases with each new frame, and that pixels in the same frame get the same iFrame
value.
#define iFrame (int(system.getTimeElapsed()*32.))
- In the code node of the shader, add a new parameter.
input_int frameCount;
#define iFrame frameCount
-
In the Shader Graph, add a
Int Parameter
node, set theScript Name
toframeCount
, and hook it up to the code node. -
In an object at the top of the scene hierachy, add the following Script Component. In the Script Component, set the material.
// @input Asset.Material material
var frameCount = 0;
function onUpdate(eventData) {
script.material.mainPass.frameCount = frameCount;
frameCount++;
}
script.createEvent("UpdateEvent").bind(onUpdate);
We can compare whether some variable float testValue
is roughly the same in Lens Studio and Shadertoy by mapping that float to a color with fragColor = floatToColor(testValue);
. This is a useful strategy to see where our Lens Studio shader differs from the original.
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
vec4 floatToColor(float value) {
// Normalize the value to the range [0, 1]
float hue = log2(value + 1.0) / log2(1001.0);
vec3 rgb = hsv2rgb(vec3(hue, 1.0, 1.0));
return vec4(rgb, 1.0);
}
Galaxy.mp4
CRT.mp4
Ascii.Camera.mp4
Using these strategies, we can even convert Iq's infamous Rainforest Shader (top) to run in Lens Studio (bottom).