docker pull biomediaicl/shaderlabweb_webgl2
docker run -p 80:3000 -d -t biomediaicl/shaderlabweb_webgl2
Install nodejs >= 14 and its package manager yarn (npm install -g yarn).
Checkout the code and install dependencies with:
git clone git@github.com:bkainz/ShaderLabWeb.git
cd ShaderLabWeb
yarn install
Run the web app locally with:
yarn serve
This serves the web app under http://localhost:3000.
a) On the server:
Install nginx (webserver) and certbot (SSL Certificate registration), e.g. on Ubuntu with:
apt install -y nginx
snap install certbot --classic
Add your SSH public key to ~/.ssh/authorized_keys to avoid entering your SSH
password multiple times during deployment.
b) On your development machine:
Change the current directory to the root directory of this repo and run:
yarn init-server user@hostname ssl@email.com 3000
with user@hostname being your SSH login to the server. server must also already
be the actual hostname (e.g. shaderlabweb.doc.ic.ac.uk) the app will be reachable
under. For this hostname an SSL certificate will be registered with ssl@email.com
being the email attached to the registered SSL certificate (through LetsEncrypt).
The email is used to warn about an expiring certificate in the case the automatic
renewal did not work as it should. 3000 is the port used internally by the nodejs
server to which nginx is a public proxy.
All of the above needs only to be done once on a newly created server. To push changes to the server follow the following deployment instructions.
On the development machine, change the current directory to the root directory of this repo and run:
yarn deploy user@hostname
with user@hostname being the same as during the server setup above.
ROOT
├── defaultStates/
│ ├── Model.json
│ │ The state loaded into a newly created model pass
│ ├── project.json
│ │ The state loaded into the app on startup and into a newly created project
│ └── Quad.json
│ The state loaded into a newly created quad pass
├── exampleFeedbackServer/
│ └── serve.js
├── public/
│ ├── assets/logo.png
│ │ The logo displayed at the top right corner
│ └── teapot.obj
│ The teapot model
├── server/
│ Scripts and files for setting up and updating a server
└── src/
├── _renderer/
│ Custom renderer and bundler to generate the output html. Unless
│ you intend to modify how the project is bundled you do not need
│ to touch this code.
├── componentHelpers/
│ Helper functions used across several components
├── components/
│ │ This folder contains the view part of the app is made of. There are
│ │ main components for the editor with webgl canvas are:
│ ├── App/Canvas/
│ │ The render window at the top left. This folder contains
│ │ all the webGl state management.
│ ├── App/Controls/Camera/
│ │ The camera tab at the bottom left.
│ ├── App/Controls/Log/
│ │ The log tab at the bottom left.
│ ├── App/Controls/Model/
│ │ The model tab at the bottom left.
│ ├── App/Controls/Uniforms/
│ │ The uniforms tab at the bottom left.
│ ├── App/Editor/
│ │ The editor to the right.
│ └── App/Header/
│ The header at the top.
├── helpers/
│ Helper functions used in models and routers
├── models/
│ Database models
├── pages/
│ Technically, pages are just like ordinary components but since they
│ are the root of the render tree with enclosing html tags their setup
│ is a little bit different.
├── routes/
│ Contains definition of the routes (i.e. URLs) of the web app.
└── defaultState.json
The state loaded into the app on startup
The project uses koa as its web framework. Routes are defined in the
routes/ directory. These routes either render a page from the pages/ directory
or a component from the components/ directory. The app uses sqlite as persistent
data storage. Records are loaded from the sqlite database via the models in the
models/ directory. Models are defined with the help of the helpers/View class
which uses helpers/database/queryBuilder.js to build its SQL queries.
Each folder in src/components with a capitalized name constitutes a component
which is a self-contained bundle of HTML markup, CSS styles and javascript scripts.
These files are rendered during the build process and combined with the files of
other components to the final html page.
Each component has the following file structure:
COMPONENT
├── index.js
│ The html markup of the component in JSX.
├── class.css
│ The style for all instances of the component. This uses ordinary CSS except
│ for the special `${className}` string which is replaced by the component's
│ class name.
├── instance.js
│ The Javascript class being instantiated for each instance of the component.
└── instance/
Additional classes imported by instance.js.
To import other js modules in instance.js use import defaultExport from 'path/to/module'.
To export the main class of the file use export default MainClassName. The same
applies to all imported modules as well.
Note: Because of the custom bundler in src/_renderer further signatures of
the ES6 import and export statements are not supported.
In general, all components are organized in a way that they only imports components that
are placed directly in their folder. The exception are a few general-purpose top-level
components like Tabs or Initializer which might be included by a component at any level.
All used instances of these components are then compiled by the code in the src/_renderer
folder to the html output.
The browser parses the HTML markup sequentially from top to bottom. Once it encounters
the root element of a component having an instance.js file it instantiates the javascript
interface in this file for the element. While doing so, it is not guaranteed that the
browser has also already parsed all descendants of the component's element, but usually
you want to do something with them somewhere during the initialization of said interface.
To have access to all these descendants the component can include the Initializer
helper component and once the browsers encounters its element, the initialize()
method of its embedding component interface will be called. Here, all descendants
encountered up to this point will be available. Therefore, it usually makes the most sense
to include the Initializer component right before the closing tag of the component.
It is probably easiest to understand this by looking at an example. The App component
is the root component containing all of the app. The constructor in its instance.js is
very short and just defines a couple members. Before its closing tag, after all sub
components for the canvas, controls and editor panes have been loaded and initialized,
it includes the Initializer component which calls App.prototype.initialize().
There, the app is actually initialized by setting its initial state, setting up the pane
resize functionality (accessing descendant elements) and finally entering the render loop.
This general initialization structure is followed by all components: They have a light-weight
constructor and if necessary a heavy-duty initialize() method triggered by an embedded
Initializer component.
While initializing the app with the default state or when loading a saved state via the
header, the app's state is set by calling the App.prototype.state setter. From there
the state is passed through the sub components by calling their state setter. All
components having a state setter also have a state getter which is used to collect
the state of all components. More details about how and where a specific state is set
or comes from can be learned about by following the state setter and getter chain through
all participating components.
When some value is changed by setting the state, it usually triggers two side-effects.
For one, the input field in the UI is changed to the new value and secondly, the update
of the corresponding value in webgl is triggered. For uniforms and vertex data this
happens by calling a method of the Canvas component. Webgl configuration like the
depth func or viewport size is updated on an indirect way by setting a value in the
app's central value registry which the Canvas component then watches for changes.
Changing a value through the UI follow exactly the same path for updating the webgl
state.
A code example for setting the webgl state via the app's central value registry can be
found in helpers/state.js, which is used by the App/Controls/Camera and
App/Controls/Model components. Setting the mesh in the App/Controls/Model component
and setting uniform values via the App/Controls/Uniforms/instance/Value.prototype.value
setter directly call methods of the Canvas.
The interface of the App component has the methods
setValue(type, name, value),getValue(type, name)andremoveValue(type, name).
These method let you interact with the app's value registry. Each value has a type and a
name. If a value is set and has a type equal to one of GLSL's recognized types int,
bool, float, ivec[234], bvec[234], vec[234] or mat[234], it appears as a
possible attachment in the attach to:-dropdown for a uniform of that type. A simple
example is the 'Time in Milliseconds' value of type int that is set in the render
loop in App.prototype.initialize().
In the web app, clicking the Get Feedback button at the bottom right in a project
takes a screenshot of the render window, collects the current state of the project
and POSTs it to URL / of the feedback server. The screenshot is sent as file
screenshot and the state is stored in field state of the body. The html response
of the feedback server is then output to the log of the web app.
The feedback server is expected to run on localhost one port above the web server, i.e.
if the web server runs under http://localhost:3000, the feedback server must run under
http://localhost:3001. And if if the web server runs under http://localhost:3002,
the feedback server must run under http://localhost:3003
There is an example feedback server written in Javascript located at
exampleFeedbackServer/serve.js in this repository. To run the feedback server
under a specific port in the background execute the command line:
$ PORT=3003 ./exampleFeedbackServer/serve.js &