Inscribing the first 1:1 WebGL + HTML5 Bitcoin Ordinal

the_garbage_man
8 min readJun 15, 2023

Background

I’m a software/hardware developer and a video artist that likes to create tech that doesn’t exist with the goal of empowering others. Doing my best to find tools that are available to everyone. I have two goals for this project. First, I want to create a single HTML5 website that renders WebGL fragment shaders without external libraries. Second, inscribe the project onto the Bitcoin blockchain as an Ordinal NFT.

Why is this important?

  • OpenGL allows for high-fidelity graphics by working directly with the GPU. WebGL is the web version of OpenGL. If you’ve used a computer in the past 30 years, you’ve experienced OpenGL graphics. For example, Three.js is a user-friendly wrapper for WebGL.
  • Empower others to push the limits of graphical possibilities on the Bitcoin blockchain.
  • The size of a shader is small, which makes it great for inscribing. Technically, there is no size limitation to shaders that I know of.
  • No external dependencies. HTML5 + Javascript is all you need.
  • A HTML5 + WebGL website example inscription hasn’t happened yet.

HTML5 Template

To start, I needed a working example which demonstrated how to run a WebGL example within a HTML5 file. The key was finding one that loads a fragment shader. Luckily, I found a working an example from Pablo Colapinto that runs locally without any external dependencies. This example is pasted below.

<html>
<script type="text/javascript">

var GL;
var shaderId;
var vertexBuffer;
var indexBuffer;
var timer = 0;

function initWebGL(){
// Get Canvas Element
var canvas = document.getElementById("glcanvas");
// Get A WebGL Context
GL = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
// Test for Success
if (!GL) {
alert("Unable to initialize WebGL. Your browser may not support it.");
}
// Set clear color to red, fully opaque
GL.clearColor(1.0, 0.0, 0.0, 1.0);
// Clear Screen
GL.clear(GL.COLOR_BUFFER_BIT| GL.DEPTH_BUFFER_BIT);

initShaderProgram();
initBuffers();
}

function initShaderProgram(){

//Create Program and Shaders
shaderId = GL.createProgram();
var vertId = GL.createShader(GL.VERTEX_SHADER);
var fragId = GL.createShader(GL.FRAGMENT_SHADER);

//Load Shader Source (source text are in scripts below)
var vert = document.getElementById("vertScript").text;
var frag = document.getElementById("fragScript").text;

GL.shaderSource(vertId, vert);
GL.shaderSource(fragId, frag);

//Compile Shaders
GL.compileShader(vertId);
GL.compileShader(fragId);

//Check Vertex Shader Compile Status
if (!GL.getShaderParameter(vertId, GL.COMPILE_STATUS)) {
alert("Vertex Shader Compiler Error: " + GL.getShaderInfoLog(id));
GL.deleteShader(vertId);
return null;
}

//Check Fragment Shader Compile Status
if (!GL.getShaderParameter(fragId, GL.COMPILE_STATUS)) {
alert("Fragment Shader Compiler Error: " + GL.getShaderInfoLog(id));
GL.deleteShader(fragId);
return null;
}

//Attach and Link Shaders
GL.attachShader(shaderId, vertId);
GL.attachShader(shaderId, fragId);
GL.linkProgram(shaderId);

//Check Shader Program Link Status
if (!GL.getProgramParameter(shaderId, GL.LINK_STATUS)) {
alert("Shader Linking Error: " + GL.getProgramInfoLog(shader));
}

}

function initBuffers(){

//Some Vertex Data
var vertices = new Float32Array( [
-1.0, -1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, 1.0, 0.0,
1.0, -1.0, 0.0
]);
//Create A Buffer
vertexBuffer = GL.createBuffer();
//Bind it to Array Buffer
GL.bindBuffer(GL.ARRAY_BUFFER, vertexBuffer);
//Allocate Space on GPU
GL.bufferData(GL.ARRAY_BUFFER, vertices.byteLength, GL.STATIC_DRAW);
//Copy Data Over, passing in offset
GL.bufferSubData(GL.ARRAY_BUFFER, 0, vertices );

//Some Index Data
var indices = new Uint16Array([ 0,1,3,2 ]);
//Create A Buffer
indexBuffer = GL.createBuffer();
//Bind it to Element Array Buffer
GL.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, indexBuffer);
//Allocate Space on GPU
GL.bufferData(GL.ELEMENT_ARRAY_BUFFER, indices.byteLength, GL.STATIC_DRAW);
//Copy Data Over, passing in offset
GL.bufferSubData(GL.ELEMENT_ARRAY_BUFFER, 0, indices );

}

//ANIMATION FUNCTION (to be passed a callback) see also http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimFrame = ( function() {

//Find best option given current browser
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||

// if none of the above, use non-native timeout method
function(callback) {
window.setTimeout(callback, 1000 / 60);
};

} ) ();

function animationLoop(){
// feedback loop requests new frame
requestAnimFrame( animationLoop );
// render function is defined below
render();
}

function render(){
timer+=.1;

//Bind Shader
GL.useProgram(shaderId);
//Update uniform variable on shader
var uID = GL.getUniformLocation(shaderId, "uTime");
GL.uniform1f(uID, timer);
//Enable Position Attribute
var attId = GL.getAttribLocation(shaderId, "position");
GL.enableVertexAttribArray(attId);
//Bind Vertex Buffer
GL.bindBuffer(GL.ARRAY_BUFFER, vertexBuffer);
///Point to Attribute (loc, size, datatype, normalize, stride, offset)
GL.vertexAttribPointer( attId, 3, GL.FLOAT, false, 0, 0);
//Bind Index Buffer
GL.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, indexBuffer);
//Draw! --( mode, number_of_elements, data type, offset )
GL.drawElements(GL.TRIANGLE_STRIP, 4, GL.UNSIGNED_SHORT, 0);
}

function start(){
initWebGL();
animationLoop();
}

</script>

<!-- VERTEX SHADER SOURCE -->
<script id="vertScript" type="text/glsl">

#ifdef GL_ES
precision lowp float;
#endif

attribute vec3 position;

void main(void) {
gl_Position = vec4(position,1.0);
}

</script>

<!-- FRAGMENT SHADER SOURCE -->

<script id="fragScript" type="text/glsl">
<!-- ADD GLSL SHADER CODE HERE -->
#ifdef GL_ES
precision lowp float;
#endif

#define PI 3.14159265359

uniform float uTime;

void main(void) {

//divide pixel location by canvas width and height
//to get values are now between 0.0 and 1.0
vec2 st = gl_FragCoord.xy/vec2(640,480);
//some fun functions for picking colors
float r = sin(uTime+8.*PI*st.x);
float g = fract( sin(3.*PI*st.y) );
float b = sin(uTime+PI*st.x*st.y);
vec3 color = vec3(r,b,g);
gl_FragColor = vec4(color,1.0);
}
</script>

<body onload = start() >
<canvas id="glcanvas" width=640 height=480 style = "margin:auto; display:block">
Oops, browser has no <code> canvas </code> tag support
</canvas>
</body>

</html>

I look for the Fragment Shader section and removed the code from inside the <script> tag. Below is what it should look like once you remove the code from the template.

...
<!-- FRAGMENT SHADER SOURCE -->
<script id="fragScript" type="text/glsl">
<!-- ADD GLSL SHADER CODE HERE -->
</script>
...

Then, I pasted my GLSL shader, or Fragment Shader, into the area where it says Add GLSL Shader Code here.

<!-- FRAGMENT SHADER SOURCE -->
<script id="fragScript" type="text/glsl">
<!-- ADD GLSL SHADER CODE HERE -->
#ifdef GL_ES
precision mediump float;
#endif

#define PI 3.14159265359

uniform vec2 uResolution;
uniform vec2 uMouse;
uniform float uTime;

float plot(vec2 st, float pct){
return smoothstep( pct-0.02, pct, st.y) -
smoothstep( pct, pct+0.02, st.y);
}

void main() {
vec2 st = gl_FragCoord.xy/vec2(640,480);
st -= 0.5;
st *= 12.0;
float pct2 = 0.0;
pct2 = distance(st,vec2(0.5));

float y3 = sin(cos((st.y)*(0.10)))-sin(st.x+st.y*02.2);
float y = smoothstep(1.2-(sin(((y3)))*(cos(st.y/-9.2))),0.5,st.x) - smoothstep(0.5,0.1,(st.x*0.2))+(sin(st.x-(uTime*.0412)));
float y2 = smoothstep(04.91,02.5,st.y+st.x) - smoothstep(0.481,0.918,st.y);
vec3 colorA = vec3(y*y2)-(y3-(0.14))*(sin(st.x-(uTime*.02412)));
colorA = (1.0)*colorA*vec3(.020+(sin(((y3+y)))+(cos(pct2*-0.2))),0.120,(0.10+(sin(y3))));

float y4 = cos(cos((st.x)*(0.13)*sin(0.23)));
float y5 = smoothstep(0.2,0.25,0.3) - smoothstep(0.25,0.31,(st.y*0.12));
float y6 = smoothstep(0.9+(st.x*0.012),0.1915,st.y+cos(st.y)) - smoothstep(0.641,0.8191,(st.x+st.y)*0.2);
vec3 colorB = vec3(y4+y5)-(y3*(0.913))*(sin(st.x+(uTime*.031592)));
colorB = (1.0)*colorB*vec3(.920,0.95030,(0.20-y6))*(sin(st.y-(uTime*.032412)));

vec2 bl = step(vec2(0.5),st);
float pct = bl.x * bl.y;
vec3 colorMix = vec3(0.0);
colorMix = mix(colorA*0.432, colorB+0.3892, 0.983*(sin(((y6+y2)))*(cos(pct2/-2.2))));

gl_FragColor = vec4(colorMix,1.0);
}
</script>

Save the file as a new index.html. Check to see if the shader rasters correctly.

We’ve tested it locally, and it runs as expected. Code is here if you are curious. It’s time to inscribe.

Inscribing the HTML file

Go to looksordinal. It is a cost-effective site for self-custodial bulk inscriptions. The website looks like the image below.

looksordinal.com

Add your Ordinal address to the Receiving Address section. Don’t forget that your Ordinal address is different than your BTC recipient address.

Choose the file you would like to inscribe. Here, it’s index.html.

Select the freerate. I’d suggest Mid because if you choose Min, you risk waiting a long time for the inscription to complete. Don’t forget to tip your dev!

Click Estimate Fees to see the cost of the inscription. Then the press the inscribe button.

The page will transition to the payment process. Scan the QR code with your preferred Bitcoin app and send the payment to the address provided.

Once the payment is received, the inscription process begins. Click the transaction link.

Track the progress of your inscription by using mempool.space.

When it’s finished inscribing, check it out on Magic Eden or Gamma.io.

Final thoughts

This is the first time a WebGL compatible HTML5 webpage has been inscribed to the Bitcoin blockchain. It is a massive moment for decentralized graphics and pushes the perception of how a blockchain can be used. It’s your turn, change the world.

FAQ

Here is primer if you are unfamiliar with concepts mentioned above.

Bitcoin Ordinals: The new Ordinals protocol allows people who operate Bitcoin nodes to inscribe each sat with data, creating something called an Ordinal. That data inscribed on Bitcoin can include smart contracts, which in turn enables NFTs. In rough terms, Ordinals are NFTs you can mint directly onto the Bitcoin blockchain.

WebGL: WebGL is a JavaScript API for rendering interactive 2D and 3D graphics within any compatible web browser without the use of plug-ins. WebGL is fully integrated with other web standards, allowing GPU-accelerated usage of physics and image processing and effects as part of the web page canvas.

GLSL Shaders: Shaders use GLSL (OpenGL Shading Language), a special OpenGL Shading Language with syntax similar to C. GLSL is executed directly by the graphics pipeline. There are several kinds of shaders, but two are commonly used to create graphics on the web: Vertex Shaders and Fragment (Pixel) Shaders. Applications using OpenGL include computer games, virtual reality, augmented reality, 3D animation, CAD and other visual simulations.

OpenGL: OpenGL (Open Graphics Library) is a cross-language, multi-platform application programming interface (API) for rendering 2D and 3D vector graphics. The API is typically used to interact with a graphics processing unit (GPU), to achieve hardware-accelerated rendering.

References

--

--

the_garbage_man

Uncomfortable confusion that makes you question your understanding of even the most basic concepts.