- Hands-On Game Development with WebAssembly
- Rick Battagline
- 1999字
- 2021-06-24 13:41:14
Adding to the virtual file system
This section is going to be a brief digression from particle systems because I would like to take the time to create a particle system design tool, which will require that we add files to the WebAssembly virtual file system. We are going to add an input element with a type of file that we can use to load an image into the virtual file system. We will need to check the file we are loading to verify it is a .png file, and if it is, we will draw and move the image around on the canvas using WebAssembly and SDL.
Emscripten does not create a virtual file system by default. Because we will need to use a virtual file system that will not initially have anything inside of it, we will need to pass the following flag to em++ to force Emscripten to build a virtual filesystem: -s FORCE_FILESYSTEM=1.
The first thing we will do is copy canvas_shell.html from Chapter 2, HTML5 and WebAssembly, and use it to create a new shell file we will call upload_shell.html. We will need to add some code into the JavaScript that will handle file loads and insert that file into the WebAssembly virtual file system. We also need to add an HTML input element of file type that will not display until the Module object has finished loading. In the following code, we have the new shell file:
<!doctype html><html lang="en-us">
<head><meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Upload Shell</title>
<link href="upload.css" rel="stylesheet" type="text/css">
</head>
<body>
<canvas id="canvas" width="800" height="600"
oncontextmenu="event.preventDefault()"></canvas>
<textarea class="em_textarea" id="output" rows="8"></textarea>
<script type='text/javascript'>
var canvas = null;
var ctx = null;
function ShowFileInput()
{document.getElementById("file_input_label")
.style.display="block";}
var Module = {
preRun: [],
postRun: [ShowFileInput],
print: (function() {
var element = document.getElementById('output');
if (element) element.value = '';
return function(text) {
if (arguments.length > 1)
text=Array.prototype.slice.call(arguments).join('
');
console.log(text);
if (element) {
element.value += text + "\n";
element.scrollTop = element.scrollHeight;
} }; })(),
printErr: function(text) {
if (arguments.length > 1)
text=Array.prototype.slice.call(arguments).join(' ');
if (0) { dump(text + '\n'); }
else { console.error(text); } },
canvas: (function() {
var canvas = document.getElementById('canvas');
canvas.addEventListener("webglcontextlost", function(e) {
alert('WebGL context lost. You will need to reload the page.');
e.preventDefault(); }, false);
return canvas; })(),
setStatus: function(text) {
if (!Module.setStatus.last) Module.setStatus.last = { time:
Date.now(), text: '' };
if (text === Module.setStatus.last.text) return;
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var now = Date.now();
if (m && now - Module.setStatus.last.time < 30) return;
Module.setStatus.last.time = now;
Module.setStatus.last.text = text;
if (m) { text = m[1]; }
console.log("status: " + text);
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
this.totalDependencies = Math.max(this.totalDependencies,left);
Module.setStatus(left ? 'Preparing... (' +
(this.totalDependencies-left) + '/' +
this.totalDependencies + ')' : 'All downloads complete.'); }
};
Module.setStatus('Downloading...');
window.onerror = function() {
Module.setStatus('Exception thrown, see JavaScript console');
Module.setStatus = function(text) { if (text) Module.printErr('[post-exception status] ' + text); };
};
function handleFiles(files) {
var file_count = 0;
for (var i = 0; i < files.length; i++) {
if (files[i].type.match(/image.png/)) {
var file = files[i];
console.log("file name=" + file.name);
var file_name = file.name;
var fr = new FileReader();
fr.onload = function (file) {
var data = new Uint8Array(fr.result);
Module.FS_createDataFile('/', file_name, data, true,
true, true);
Module.ccall('add_image', 'undefined', ["string"],
[file_name]);
};
fr.readAsArrayBuffer(files[i]);
}
}
}
</script>
<input type="file" id="file_input" onchange="handleFiles(this.files)" />
<label for="file_input" id="file_input_label">Upload .png</label>
{{{ SCRIPT }}}
</body></html>
In the header, the only changes we are making are to the title, and the style sheet:
<title>Upload Shell</title>
<link href="upload.css" rel="stylesheet" type="text/css">
In the body tag, we are leaving the canvas and textarea elements alone, but there are significant changes to the JavaScript. The first thing we will do to the JavaScript is to add a ShowFileInput function to display the file_input_label element, which starts as hidden by our CSS. You can see it in the following code snippet:
function ShowFileInput() {
document.getElementById("file_input_label").style.display = "block";
}
var Module = {
preRun: [],
postRun: [ShowFileInput],
Notice that we have added a call to this function in our postRun array so that it runs after the module is loaded. That is to make sure no one loads an image file before the Module object has loaded and our page can handle it. Aside from the addition of ShowFileInput to the postRun array, the Module object is unchanged. After our Module object code, we added a handleFiles function that is called by our file input element when the user picks a new file to load. Here is the code for that function:
function handleFiles(files) {
var file_count = 0;
for (var i = 0; i < files.length; i++) {
if (files[i].type.match(/image.png/)) {
var file = files[i];
var file_name = file.name;
var fr = new FileReader();
fr.onload = function (file) {
var data = new Uint8Array(fr.result);
Module.FS_createDataFile('/', file_name, data, true,
true, true);
Module.ccall('add_image', 'undefined', ["string"],
[file_name]);
};
fr.readAsArrayBuffer(files[i]);
}
}
}
You will notice that the function is designed to handle multiple files at once by looping over the files parameter passed into handleFiles. The first thing we will do is check to see if the image file type is PNG. When we compile the WebAssembly, we need to tell it what image file types SDL will handle. The PNG format should be all you need, but it is not difficult to add other types here.
If you do not want to check for PNG files specifically, you can leave out the .png part of the match string and later add additional file types into the compile command-line parameters. If the file is an image/png type, we put the filename into its variable, file_name, and create a FileReader object. We then define the function that runs when the FileReader loads the file:
fr.onload = function (file) {
var data = new Uint8Array(fr.result);
Module.FS_createDataFile('/', file_name, data, true, true, true);
Module.ccall('add_image', 'undefined', ["string"], [file_name]);
};
This function takes in the data as an 8-bit unsigned integer array and then passes it into the Module function, FS_createDataFile. This function takes as its parameters a string that is the parent directory '/' of our file, the filename, file_name, the data we read from our file, followed by canRead, canWrite, and canOwn, which should all be set to true because we would like to be able to have our WebAssembly read, write, and own this file. We then use Module.ccall to call a function defined in our WebAssembly called add_image that will take the filename so that our WebAssembly can render this image to the HTML canvas using SDL. After we define the function that tells the FileReader what to do when a file is loaded, we have to instruct the FileReader to go ahead and read in the loaded file as an ArrayBuffer:
fr.readAsArrayBuffer(files[i]);
After the JavaScript, we added a file input element and a label to go along with it, as shown here:
<input type="file" id="file_input" onchange="handleFiles(this.files)" />
<label for="file_input" id="file_input_label">Upload .png</label>
The label is purely for styling. Styling an input file element is not a straightforward thing in CSS. We will go over how to do that in a little bit. Before discussing the CSS, I would like to go over the WebAssembly C code that we will use to load and render this image using SDL. The following code will go inside of a file we have named upload.c:
#include <emscripten.h>
#include <stdlib.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
SDL_Window *window;
SDL_Renderer *renderer;
char* fileName;
SDL_Texture *sprite_texture = NULL;
SDL_Rect dest = {.x = 160, .y = 100, .w = 16, .h = 16 };
int sprite_x = 0;
int sprite_y = 0;
void add_image(char* file_name) {
SDL_Surface *temp_surface = IMG_Load( file_name );
if( !temp_surface ) {
printf("failed to load image: %s\n", IMG_GetError() );
return;
}
sprite_texture = SDL_CreateTextureFromSurface( renderer,
temp_surface );
SDL_FreeSurface( temp_surface );
SDL_QueryTexture( sprite_texture,
NULL, NULL,
&dest.w, &dest.h );
}
void show_animation() {
if( sprite_texture == NULL ) {
return;
}
SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );
SDL_RenderClear( renderer );
sprite_x += 2;
sprite_y++;
if( sprite_x >= 800 ) {
sprite_x = -dest.w;
}
if( sprite_y >= 600 ) {
sprite_y = -dest.h;
}
dest.x = sprite_x;
dest.y = sprite_y;
SDL_RenderCopy( renderer, sprite_texture, NULL, &dest );
SDL_RenderPresent( renderer );
}
int main() {
printf("Enter Main\n");
SDL_Init( SDL_INIT_VIDEO );
int return_val = SDL_CreateWindowAndRenderer( 800, 600, 0, &window,
&renderer );
if( return_val != 0 ) {
printf("Error creating renderer %d: %s\n", return_val,
IMG_GetError() );
return 0;
}
emscripten_set_main_loop(show_animation, 0, 0);
printf("Exit Main\n");
return 1;
}
There are three functions we have defined inside of our new upload.c file. The first function is the add_image function. This function takes in a char* string that represents the file we have just loaded into the WebAssembly virtual file system. We use SDL to load the image into a surface, and then we use that surface to create a texture we will use to render the image we loaded. The second function is show_animation, which we use to move the image around the canvas. The third is the main function, which always gets run when the module is loaded, so we use it to initialize our SDL.
Let's take a quick look at the add_image function:
void add_image(char* file_name) {
SDL_Surface *temp_surface = IMG_Load( file_name );
if( !temp_surface ) {
printf("failed to load image: %s\n", IMG_GetError() );
return;
}
sprite_texture = SDL_CreateTextureFromSurface( renderer,
temp_surface );
SDL_FreeSurface( temp_surface );
SDL_QueryTexture( sprite_texture,
NULL, NULL,
&dest.w, &dest.h );
}
The first thing we do in the add_image function is use the file_name parameter we passed in to load an image into an SDL_Surface object pointer, using the IMG_Load function that is a part of the SDL_image library:
SDL_Surface *temp_surface = IMG_Load( file_name );
If the load fails, we print an error message and return from the function:
if( !temp_surface ) {
printf("failed to load image: %s\n", IMG_GetError() );
return;
}
If it does not fail, we use the surface to create a texture that we will be able to render in the frame animation. Then, we free the surface because we no longer need it:
sprite_texture = SDL_CreateTextureFromSurface( renderer, temp_surface );
SDL_FreeSurface( temp_surface );
The final thing we do is use the SDL_QueryTexture function to get the image's width and height, and load those values into the dest rectangle:
SDL_QueryTexture( sprite_texture,
NULL, NULL,
&dest.w, &dest.h );
The show_animation function is similar to other game loops we have written in the past. It should run every frame, and as long as a sprite texture is loaded, it should clear the canvas, increment the sprite's x and y values, and then render the sprite to the canvas:
void show_animation() {
if( sprite_texture == NULL ) {
return;
}
SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );
SDL_RenderClear( renderer );
sprite_x += 2;
sprite_y++;
if( sprite_x >= 800 ) {
sprite_x = -dest.w;
}
if( sprite_y >= 600 ) {
sprite_y = -dest.h;
}
dest.x = sprite_x;
dest.y = sprite_y;
SDL_RenderCopy( renderer, sprite_texture, NULL, &dest );
SDL_RenderPresent( renderer );
}
The first thing we do in show_animation is to check if the sprite_texture is still NULL. If it is, the user has not loaded a PNG file yet so we can not render anything:
if( sprite_texture == NULL ) {
return;
}
The next thing we will do is clear the canvas with the color black:
SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );
SDL_RenderClear( renderer );
Then, we will increment the sprite's x and y coordinates and use those values to set the dest (destination) rectangle:
sprite_x += 2;
sprite_y++;
if( sprite_x >= 800 ) {
sprite_x = -dest.w;
}
if( sprite_y >= 600 ) {
sprite_y = -dest.h;
}
dest.x = sprite_x;
dest.y = sprite_y;
Finally, we render the sprite to the back buffer, and then move the back buffer to the canvas:
SDL_RenderCopy( renderer, sprite_texture, NULL, &dest );
SDL_RenderPresent( renderer );
The final function in upload.c is the main function, which gets called when the module is loaded. This function is used for initialization purposes and looks like this:
int main() {
printf("Enter Main\n");
SDL_Init( SDL_INIT_VIDEO );
int return_val = SDL_CreateWindowAndRenderer( 800, 600, 0, &window,
&renderer );
if( return_val != 0 ) {
printf("Error creating renderer %d: %s\n", return_val,
IMG_GetError() );
return 0;
}
emscripten_set_main_loop(show_animation, 0, 0);
printf("Exit Main\n");
return 1;
}
It calls a few SDL functions to initialize our SDL renderer:
SDL_Init( SDL_INIT_VIDEO );
int return_val = SDL_CreateWindowAndRenderer( 800, 600, 0, &window, &renderer );
if( return_val != 0 ) {
printf("Error creating renderer %d: %s\n", return_val,
IMG_GetError() );
return 0;
}
Then, it sets up the show_animation function to run every time we render a frame:
emscripten_set_main_loop(show_animation, 0, 0);
The final thing we will do is set up a CSS file to display the HTML in our shell file correctly. Here are the contents of the new upload.css file:
body {
margin-top: 20px;
}
#output {
background-color: darkslategray;
color: white;
font-size: 16px;
padding: 10px;
margin-left: auto;
margin-right: auto;
display: block;
width: 780px;
}
#canvas {
width: 800px;
height: 600px;
margin-left: auto;
margin-right: auto;
display: block;
background-color: black;
margin-bottom: 20px;
}
[type="file"] {
height: 0;
overflow: hidden;
width: 0;
display: none;
}
[type="file"] + label {
background: orangered;
border-radius: 5px;
color: white;
display: none;
font-size: 20px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
margin-left: auto;
margin-right: auto;
width: 130px;
padding: 10px 50px;
transition: all 0.2s;
vertical-align: middle;
}
[type="file"] + label:hover {
background-color: orange;
}
The first few classes, body, #output, and #canvas, are not much different from the version of those classes we had in previous CSS files, so we do not need to go into any detail on those. After those classes is a CSS class that looks a little different:
[type="file"] {
height: 0;
overflow: hidden;
width: 0;
display: none;
}
That defines the look of an input element that has a type of file. For some reason, using CSS to style a file input element is not very straightforward. Instead of styling the element directly, we will hide the element with the display: none; attribute and then create a styled label, like this:
[type="file"] + label {
background: orangered;
border-radius: 5px;
color: white;
display: none;
font-size: 20px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
margin-left: auto;
margin-right: auto;
width: 130px;
padding: 10px 50px;
transition: all 0.2s;
vertical-align: middle;
}
[type="file"] + label:hover {
background-color: orange;
}
That is why, in the HTML, we have a label element immediately after our input file element. You may notice that our label also has set the display to none. That is so that the user can not use the element to upload a PNG file until after the Module object is loaded. If you look back to the JavaScript inside of our HTML shell file, we called the following code on postRun so that the label becomes visible after our Module is loaded:
function ShowFileInput() {
document.getElementById("file_input_label").style.display =
"block";
}
Now, we should have an app that can load an image into the WebAssembly virtual file system. In the next several sections, we will expand this app to configure and test a simple particle emitter.