Documentation
In order to get started with Pico GPU, let's explain the main interface elements first (see picture above);
Code: this allows you to edit your CPU code. The CPU code is run once when you start your app, then the
update()function is called every frame. The CPU code will execute GPU draw calls in order to display things on screen.Render : in the top right of the window, you can see your app being rendered. The app will restart when you change the code, a shader, the memory, etc.
Log : at the bottom right of the window, you can log what you want using the
trace()command in your CPU code.Shaders : in this section, you can edit your different shaders. You can have up to 16 different shaders, and they can be referenced by number in your CPU code when using
setShader()orsetShaders()functions. Pico GPU uses runtime shader assembling you can actually have more than 16 shaders by combining different shaders when performing a draw. See Shaders section below for more details.Memory : your Pico GPU can use up to 300KB of memory. You can have up to 16 different memory buffers to store textures, vertex or index buffers, but also render targets or depth textures. See Memory section below for more details.
Samples : Pico GPU comes with some samples that you can edit and modify freely to experiment with shaders.
Run : you can press
F5to run your app at any time, it will start immediately. UseEscto exit and return to edit mode. Using Ctrl+F5 can start it in fullscreen (if run with HashLink).Save/Load : you can load & save your app as a PNG file that will contain all your app source data. If you want to Save as a new file you can hold Ctrl when clicking Save. You can also use Ctrl+S at any time to save your app.
Code
The code input follows the Haxe syntax and is run using the HScript interpreter.
Syntax
You can read a complete syntax guide in haxe manual, but if you are familiar with C or Javascript, this should be pretty straightforward.
Please note that PicoGPU code is type checked, so you will get an error if you use an invalid identifier or make type errors additionally to syntax errors. The errors are displayed in real time as you type, so no need for compilation.
Execution
When something is modified, the PicoGPU code will get executed. Then every frame the update() function will be called, so you can display one frame. Here's a small example that will simply clear the screen every frame:
var time = 0.;
function update() {
time += 1/60;
clear(vec4(time%1));
}
Drawing
In order to draw things on screen, you need to do the following:
- configure your draw call with functions such as
cull() - select your shaders using
selectShaderorselectShaders. See Shaders section below for more details. - set the shader parameters with
setParamandsetGlobal - set the camera with
setCamera - perform one or several draw calls with
draw()ordrawInstance()using one or several buffers that contains your GPU data.
API
In order to know what are the functions you can use, you have access to the following:
- all methods that are part of the
PicoApiclass can be accessed globally. See list here - The
Key,Blend,Compare,StencilandMathclasses are also globally accessible - The
trace()function can add something to the log, for debugging purposes.
Screenshot
When saving, the function screenshot() {} function will be called instead of update() if it exists, allowing you to display a custom screen that will be used for your save PNG.
Code API
This is a complete list of the available API functions:
Memory
loadBuffer(buffer:Int):Buffer
Load the memory buffer at the given Memory index [0-15].loadTexture(buffer:Int):Texture
Load the texture from the memory buffer at the given Memory index.loadStorage(name:String):Buffer
Load a 256 bytes persistent storage buffer with the given unique name. This can be used to save your game progression. The buffer will be saved on the local browser/computer for all apps and can be read or written by other apps so make sure to have a unique specific name such asauthor(game-name).
System
trace(arg1:*,?arg2:*,...)
Output any number of values to the log. This will also display the current script line number.error(value:*)
Output an error in red in the log. Do not repeat if the same error is already the latest log message.stop()
Stop the app execution.time() : Float
Return the time in seconds since the app started.date() : Float
Return the current date in seconds since January 1st 1970.
Shaders
setShader(shader:Int)
Set the current shader used for drawing. Theshaderindex is a [0-15] integer, one of the 16 shaders defined for your program.setShaders(shaders:Array<Int>)
Set the current shaders a combination of several shaders. This can be used to separate vertex & fragment shaders, or add special effects that only affect some intermediary calculus.setParam(name:String,value:*)
Set the parameter for current shader(s) that was set withsetShader. Parameters are declared in shaders with@paramand served as configuring the shader. The value can be an Int, Float, Vector, Matrix, Texture, Buffer, etc. It must match the shader declaration type or an error might occur when drawing. It is not possible to set a sound shader parameter, so use setGlobal instead.setGlobal(name:String,value:*)
This is similar to shader parameters except that globals are shared among all shaders and must be declared in shaders with@global.setCamera(m:Matrix,?fovY:Float=25)
This will set the globalcameraViewProjwith a correct perspective matrix with given vertical field of view angle (fovY). Themmatrix gives the camera position and rotation. This is an utility function and you can directly set thecameraViewProjyourself.
Controls
button() : Bool
Tells if the main button is active. This can be theEorSpacekeys, or the padAorXbuttons, or the left mouse click.button2() : Bool
Tells if the secondary button is active. This can be theRorEnterkeys, or the padBorYbuttons, or the right mouse click.dirX() : Float
Tells the X direction we are moving. This can be with horizontal movement with keyboardWASDor arrow keys, or the game pad analog control (which will return values that can be < 1) or dpad.dirY() : Float
Similar todirX()but gives the vertical direction.mouseX() : Int
Gives the mouse X position on screen.mouseY() : Int
Gives the mouse Y position on screen.
Drawing
draw(buffer:Buffer)
perform a draw call using the current selected shader and the given vertex buffer that contains vertex data. See shaders to understand the structure of the buffer.draw(buffer:Buffer,index:Buffer)
same adraw(buffer)but use a index buffer that will store three I32 for each triangle. This allows to reuse the vertices in several triangles without having them repeated in the vertex buffer.draw(buffer:Buffer,?index:Buffer,?startTri:Int=0,?drawTri:Int=-1)
This is the complete signature of thedraw()function, you can additionally draw a sub part of the buffer by specifying a starting triangle asstartTriand the number of triangles to draw indrawTridrawText(text:String)
This will create a buffer of glyphs with (position:XY,uv:UV) format and draw it on screen. Positions are in screen pixel format and needs to be converted to screen position, and UV are used to sample the globaltextFonttexture. See the sample TextHelloWorld for a full sample.drawInstance(buffer:Buffer,instanceBuffer:Buffer,count:Int,?index:Buffer)
This allows you to perform several draw in a single call. All@perInstanceshader parameters are stored in theinstanceBufferandcounttells how many instances to draw. See theDrawInstancedsample for a complete tutorial. The main advantage ofdrawInstanceis better performances for static data that needs to be draw every frame.clear(color:Vec4)
Clear the current target texture with the given RGBA color value.clearDS(?depth:Float,?stencil:Int)
Clear, the depth and/or stencil values for the current target texture.
Drawing Configuration
These functions allow you to customize the way the drawing calls will render on the screen.
cull(face:Int)
Change the face culling for the draw calls. You can usecull(0)to disable culling, so your triangle will be draw from both faces, orcull(1)so that all forward facing triangles will be discarded, orcull(-1)to discard backward facing triangles.alpha(b:Bool)
Enable or disable alpha blending (transparency). You can customize blending even further with theblend()function.clip(x:Int=0,y:Int=0,width:Int=-1,height:Int-1)
Set a rectangle that will restrict all draw calls to this area. Every pixel outside will be discarded.depth(b:Bool)
Enable or disable depth comparison. When enabled, fragment pixels which have a higher Z will be discarded if there was a previous pixel with same or lower Z (more in front of the camera). When disabled, all pixels are always drawn and the draw call doesn't write into the depth buffer. You can customize even further with thedepthCompfunction.showTexture(tex:Texture)
After finishing the update, will display the given texture on screen instead of the default output. This can be used as a shortcut to show a texture without a draw() or for debugging purposes to display what was draw into a render target.
Advanced configuration
These functions allow you to further configuration the graphics rendering, but are more advanced/complex:
blend(src:Blend,dst:Blend)
change the blending between the source (your shader pixel) and the destination (the target pixel it blends with).
This will do the formulaoutputColor = shaderPixel * srcBlend + currentPixel * dstBlend.
For instance to have opaque pixels, you can doblend(Blend.ONE,Blend.ZERO)which will dooutputColor = shaderPixel * 1 + currentPixel * 0 = shaderPixel.
Or if you want to perform transparency based on the alpha value of your shader pixel, useblend(Blend.SRC_ALPHA,Blend.ONE_MINUS_SRC_ALPHA)which will dooutputColor = shaderPixel * shaderPixel.a + currentPixel * (1 - shaderPixel.a).
There are many other combinations available with the different blending values.depthComp(comp:Compare,write:Bool=true)
Set the depth comparison function, which will tell how to discard pixels. Also indicate if the draw call will or will not write into the depth buffer.colorMask(mask:Int)
Change which RGBA channels are written in the output texture. For instancecolorMask(1)will only write the red channel,colorMask(10)will write the alpha (8) and green (2) channels, andcolorMask(15)will write all RGBA channels.stencil(op:StencilOp,fail:StencilOp,pass:StencilOp,front:Bool=true)
Change the stencil operations for one of the two faces. Stencil operation are a complex but powerful way to display or hide some pixels based on previous draw calls. We won't document here the details on how to use stencil in details, but you can easily find some tutorials on the web.stencilFunc(comp:Compare,reference:Int=0,readMask:Int=0xFF,writeMask:Int=0xFF))`
Change the stencil comparison function, reference value and read & write bit masks.setTarget(?t:Texture,?depth:Buffer)
Change the rendering so all further draw operations are done on the target texture. Ifdepthis specified, then this will also attach a depth buffer to the target texture.
UsesetTarget()to return to the drawing on screen and default depth buffer.
Sound
setChannel(channel:Int,shader:Int)
Set the shader for the given sound channel.channelis a [0-3] index for one of the 4 sound channels.shaderis an index for a shader writing thesoundFloat variable. You can set the shader to-1to disable the sound channel.
Maths
mat4(?arr:Array<Float>):Matrix
Create a new 4x4 Matrix. This can be used to return an identity matrix withmat4()or load a matrix from 16 floats as a array.vec4(?x:Float=0,?y:Float,?z:Float,?w:Float):Vector4
Create a new 4-components vector. This can be used to create a single vector which all values have the same float value withvec4(1)for example.vec3(?x:Float=0,?y:Float,?z:Float):Vector
Create a new 3-components vector. This can be used to create a single vector which all values have the same float value withvec3(1)for example.quat(?x:Float=0,?y:Float,?z:Float,?w:Float):Quat
Create a 4-components Quaternion that can be used to represent a 3D rotation.rnd(max:Float=1.0):Float
Return a random Float number between 0 and the max specified (1 by default).random(max:Int):Int
Return a random integer number between 0 and the max specified. abs(v:Float) : Float;
Standard math functions
These are not documented as they are pretty straightforward. If you want some details you can them on this page
cos(v:Float) : Floatsin(v:Float) : Floattan(v:Float) : Floatacos(v:Float) : Floatasin(v:Float) : Floatatan(v:Float) : Floatatan2(y:Float,x:Float) : Floatceil(v:Float) : Floatfloor(v:Float) : Floatround(v:Float) : Floatexp(v:Float) : Floatlog(v:Float) : Floatmin(a:Float,b:Float) : Floatmax(a:Float,b:Float) : Floatimin(a:Int,b:Int) : Intimax(a:Int,b:Int) : Intpow(a:Float,b:Float) : Floatsqrt(v:Float) : Float
Buffer Object
buf.length : Int
The buffer length in number of bytes. Read only.buf.getI32(n:Int):Int
Read the n-th int32 and returns it.buf.getF32(n:Int):Float
Read the n-th float32 and returns it.buf.setI32(n:Int,v:Int)
Set the n-th int32 value. Changing the buffer data will require setting it again for shaders.buf.setF32(n:Int,v:Float)
Set the n-th float32 value. Changing the buffer data will require setting it again for shaders.buf.setVec(n:Int,v:Vec4)
Shortcut to set the four consecutive Floats components of the Vec4buf.setMat(n:Int,m:Matrix)
Shortcut to set the 16 consecutive Floats components of the Matrix.buf.setMat3x4(n:Int,m:Matrix)
Shortcut to set the 12 consecutive Floats components of the Matrix.buf.getTexture(?fmt:Format):Texture
Converts the buffer into a texture with the given format.
Texture Object
tex.width : Int
The width in pixels of the texture. Read only.tex.height : Int
The height in pixels of the texture. Read only.tex.format : Format
The format of the texture. Read only.tex.filter(b:Bool)
Change the filtering mode of the texture. When enable, the texture samples will use bilinear filtering. When disabled (default), it will return the nearest pixel.tex.wrap(b:Bool)
Change the wrapping mode of the texture. When enable (default), sampling the texture outside of the [0-1] coordinates will wrap. When disabled, it will clamp to the border pixel.tex.isDisposed() : Bool
Tells if the texture has been disposed (because its corresponding buffer has been modified).
Memory
PicoGPU allows to create up to 16 cpu-gpu data banks, with up to 300KB of total memory. These data banks can be used:
- as CPU memory, they can be read and written
- as GPU memory, for representing a vertex or index buffer
- as GPU memory, for representing a texture
There are several types of data banks:
- Undef is empty data bank that can be initialized
- I32 contains an array of 32 bit integer values.
- F32 contains an array of 32 bit float values.
- Texture is a reference to texture pixels, additionally to a width,height and pixel format.
You can initialize a memory bank by switching its type. Click on the Undef (or other type) button and select the type you want.
You can also select Import in order to import either a WAV file (which will be converted to a 48Khz F32 buffer) or any binary file (which will be kept as raw I32 bytes).
In I32 and F32 mode, there are two possible syntax: either [1,-1,0,4...] with one number per element or [0][256] for an array of 256 elements all initialized with 0. If you are using one number per element, you can adjust the display stride (number of elements per line) with the + and - buttons.
The Size is the amount of memory for this bank. It will be 4 bytes per array element or texture pixel.
Loading Memory
You can load your memory bank in code using loadMemory(index) with index being 0 to 15 slot index of your memory bank. Once a memory bank is loaded, you can read/write it with CPU code setI32/setF32 and other methods. You can then convert the memory bank to a texture using bank.getTexture(), or use the shortcut loadTexture(index) which is similar to loadMemory(index).getTexture().
Memory buffers
When drawing, your buffer memory layout must match your shader input. So if you have two inputs @input var pos : Vec3 and @input var uv : Vec2 then you need to have 5 float32 per pertex, in the order declared in your shader. For index buffers, it's one I32 per index.
Shaders
This is a simple shader:
var pos : Vec3;
var cameraViewProj : Mat4;
var outputPosition : Vec4;
var outputColor : Vec4;
function vertex() {
outputPosition = vec4(pos,1) * cameraViewProj;
}
function fragment() {
outputColor = vec4(1);
}
Drawing 3D content on screen will be performed in several steps:
First, the vertexes buffer that you pass as parameter to the draw() call contain several vertex. Each of these vertex will go through the vertex shader first.
For each vertex, the data is extracted from the vertexes buffer based on the @input shader variables. In this shader case, we expect the vertex buffer to contain three floats (X,Y,Z) per vertex, which will be loaded into the pos variable.
Then for each vertex, the function vertex() is called inside the shader. This is called the vertex shader. Please note that shaders are executed by the GPU, so the code here is slightly different than in your application code.
The role of the vertex shader is to transform each vertex coordinate from a 3D position into a 2D one that is on screen. This is performed by the operation vec4(pos,1) * cameraViewProj which will expand the (X,Y,Z) into (X,Y,Z,1) vector, then multiply it by the camera view and projection matrix to get a new 2D position (X,Y,Z,W). The X and Y coordinates are screen coordinates in the [-1,1] range, (0,0) beeing the center of the screen. And the Z and W coordinates are used for perspective correction and depth calculus. Once you have written the outputPosition the vertex shader has finished its job.
Once three vertex have been projected in 2D screen coordinates, they form a triangle and this triangle will be rasterized on screen, pixel by pixel. In order to compute the color of each pixel, we need to use a fragment shader (also called pixel shader).
In our example, for each pixel we will return the white opaque pixel which is (1,1,1,1), as four values between [0-1] in RGBA values. You can change it to vec4(1,0,0,1) to have an opaque red for example.
Testing
In order to test this shader, you can use the following code:
// setup the camera
var m = mat4();
m.rotate(1,2,3);
m.translate(0,0,4);
setCamera(m);
// draw every frame
function update() {
var buf = loadBuffer(0);
setShader(0);
draw(buf);
}
And you need to set the Memory 0 bank to F32 and enter the following value that will represent a 3D cube (X,Y,Z) x 12 faces = 36 values.
[
-0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5,
-0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5,
0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5,
0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5,
-0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5,
-0.5, -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5,
-0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5,
-0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5,
-0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5,
-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
-0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5,
-0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5,
]
Textures
Texture are images that are stored on the GPU that can be read, usually in the fragment shader, in order to display some pixels on the triangle.
In order to read a texture, you need first to add an image to the Memory bank #1. Open it and change from Undef to Texture. Choose a texture that is not too big for PicoGPU memory. Then add the following line in your update() function before the draw() and after the setShader(0):
setParam("tex",loadTexture(1));
And change your fragment shader with the following code:
var tex : Sampler2D;
function fragment() {
outputColor = tex.get(vec2(0,0));
}
This will change the color of your cube. However you will notice that the color is uniform. This is because for each pixel calculated by the fragment shader, we are only reading the top left pixel of the texture at (0,0). If we want to map the texture to our cube face, we need to use texture coordinates (also called UV coordinates).
These can be written into variable by the vertex shader (one value per each vertex). They will then get interpolated for each pixel so you will get a unique per pixel value in the fragment shader.
Here's our final shader code:
var pos : Vec3;
var cameraViewProj : Mat4;
var outputPosition : Vec4;
var outputColor : Vec4;
var uv : Vec2;
function vertex() {
outputPosition = vec4(pos,1) * cameraViewProj;
uv = pos.zy + vec2(0.5);
}
var tex : Sampler2D;
function fragment() {
outputColor = tex.get(uv);
}
In order to correctly map all of the faces of your cube, you will need appropriate UV coordinates per vertex in your vertexes buffer.
Sound
In order to synthetize sound & musics, you have a single function that allows you to assign a shader to one of the four sound channels: setChannel(0,1) will assign the shader 1 to the sound channel 0.
The shader will then need to generate some data by writing the sound variable with a value between -1 and 1.
Here's a very simple single-note 440Hz sound shader:
var time : Float;
var sound : Float;
function fragment() {
var t = time * 440;
sound = sin(t.fract() * 2 * PI);
}
Playing notes
And here's a more complex one playing several notes:
var time : Float;
var sound : Float;
// the partition (3 notes)
var notes = [0,1,2,2,0,1,2,2,0,2,1,0,1,2];
// the frequency of each note
var freq = [440,349,293];
// the number of notes played per second
var bpm = 4;
function fragment() {
var n = notes[int(time*bpm)%notes.length];
var t = freq[n] * time;
sound = sin(t.fract() * PI * 2);
}
You can try this sample by loading the Sound sample.
Or if you prefer a more old 8-bit style square wave:
sound = step(0.5,t.fract());
Playing custom sounds
In order to play WAV sounds, you must first import them into a Memory Buffer. Use the Memory Import function to convert the WAV into the corresponding 32 bit F32 buffer. Then you can read this buffer in your shader to output it into the sound channel. An example of this can be found in the Sound Sample.