Skip to content
forked from lume/glas

WebGL in WebAssembly with AssemblyScript

License

Notifications You must be signed in to change notification settings

lukebrowell/glas

 
 

Repository files navigation

glas

WebGL in WebAssembly with AssemblyScript.

This is a work-in-progress port of Three.js, a JavaScript 3D WebGL library, to AssemblyScript.

motivation

It'd be sweet to have a high-performing WebGL engine that runs in the web via WebAssembly and is written in a language that web developers are already familiar with: JavaScript, in the form of TypeScript (a superset of JavaScript with types).

Enter AssemblyScript, a toolchain that allows us to write a strictly-typed subset of TypeScript code and compile it to WebAssembly (an assembly-like language representing machine code) for speed.

status

At the moment nothing renders to the screen yet, but we have already ported a number of Three.js classes to AssemblyScript along with their unit tests, and are making progress towards having a first demo.

See the current progress in the project board.

In the Initial Port project board we're tracking all the classes that need to be ported. The initial goal is to reproduce the following basic Three.js demo, but entirely AssemblyScript:

https://codepen.io/trusktr/pen/EzBKYM

get involved

The work currently consists of picking a Three.js class and re-writing it from JavaScript (with TypeScript declaration files) to AssemblyScript (effectively merging the .js and .d.ts files).

Most logic can be ported unchanged, but sometimes there are features of plain JS that AssemblyScript does not support, in which case we need to re-write it in a different way. As an example, APIs that accept plain object literals with arbitrary properties are not acceptable in AssemblyScript. Such things need to be ported to structures that have specific shapes defined with classes.

We also port over the *.test.js files that contain unit tests, and rename them to *.spec.ts files.

As an example, the Three.js files src/math/Matrix4.js, src/math/Matrix4.d.ts, and test/unit/math/Matrix4.tests.js get ported to the glas equivalent of src/as/math/Matrix4.ts and src/as/math/Matrix4.spec.ts.

For comparison, see the original files from Three.js,

and the ported files within glas:

Once a set of Three.js files (.js, .d.ts, and .tests.js) are ported, we can run the unit tests (or more conveniently run them as we go during porting).

To run the tests, run the following commands in your terminal:

# make sure dependencies are installed first
npm install

# run unit tests
npm test

The output will tell you which tests pass and which tests fail.

project structure

project
  ├── build/ # contains build output after running `npm run build`. This structure mirrors that of the src/ folder.
  ├─┬ src/
  | ├─┬ as/ # contains AssemblyScript code which is compiled into a WebAssembly module. This code runs inside the WebAssembly environment. The code in here mirrors the structure the src/ folder in the Three.js repository.
  | | ├── index.ts # entry point for the WebAssembly module.
  | | └── tsconfig.json # AssemblyScript compiler settings for WebAssembly─side code
  | ├─┬ ts/ # contains TypeScript code which runs on the JavaScript side. This code loads and runs the WebAssembly module in an HTML page.
  | | ├── index.ts # entry point for JavaScript─side code
  | | └── tsconfig.json # TypeScript compiler settings for JavaScript─side code
  | ├── infra/ # contains infrastructure code (f.e. for the static file server)
  | └── index.html # the index file that will be served to your browser. This loads the JavaScript-side entry point, which in turn runs the WebAssembly module.
  └── *.* # any files at the root of the project are meta files like package.json, editorconfig, etc.

development process in more detail

The porting process is a learning process consisting of trial and error learning how AssemblyScript works and adapting the JavaScript code to work in AssemblyScript. Much of the code can remain mostly the same, but certain things that work in JavaScript, like object literals, don't work in AssemblyScript. In these cases we've needed to convert things like object literals to strictly typed instances constructed from classes that we defined and which had not previously existed in the Three.js code base.

For example, functions accepting options objects like

function someFunction(options) {
	// ...
}
someFunction({option1: 'foo', option2: 'bar'})

have needed to be converted to something strictly typed like so:

class SomeFunctionOptions {
	option1: string
	option2: string
}
function someFunction(options: SomeFunctionOptions) {
	// ...
}
someFunction({option1: 'foo', option2: 'bar'} as SomeFunctionOptions)

We've also needed to convert Three.js function-style classes to class syntax in the AS code. For example, a class like

function Mesh() {
	Object3D.call(this)
	// ...
}

Mesh.prototype = Object.create(Object3D.prototype, {
	someMethod() {
		// ...
	},
})

has needed to be converted to a class of the form

class Mesh extends Object3D {
	constructor() {
		super()
		// ...
	}

	someMethod() {
		// ...
	}
}

The EventDispatcher class is a prime example of that needed some refactoring to allow for strict typing to work within the confines of AssemblyScript. For example, in Three.js, listening to and dispatching an event looks like the following:

obj.addEventListener('didSomething', event => {
	log(event.target === obj) // true
	log(event.foo) // 123
	log(event.lorem) // 456
})
obj.dispatchEvent({type: 'didSomething', foo: 123, bar: 456})

As passing object literals like that doesn't work in AssemblyScript (we do not know what properties someone would want to pass within an event), we changed the API to look like this:

class SomeThing extends Listener {
	handleEvent(e: Event) {
		if (e.type == 'didSomething' && e.target === obj) {
			log(event.target === obj) // true

			// we need to use a conditional statement so that AssemblyScript can
			// narrow the type of event.attachment to that which we expect.
			const attachment = event.attachment
			if (attachment instanceof SomethingEventAttachment) {
				log(event) // 123
				log(event.lorem) // 456
			}
		}
	}
}

// listeners must be objects with a handleEvent method.
const listener = new SomeThing()

obj.addEventListener('didSomething', listener)

class SomethingEventAttachment {
	foo: f64
	bar: f64
}

const attachment: SomethingEventAttachment = {foo: 123, bar: 456}

obj.dispatchEvent(new Event('didSomething', obj, attachment))

We will improve the Event API over time, but that's the initial draft. Once AssemblyScript releases the new function closures feature, we can switch back to passing functions, instead of objects with handleEvent methods. Another improvement (help wanted) is to make the dispatchEvent method automatically create the Event instance internally.

The process we have been taking so far is choosing a class from Three.js, and sticking it into the src/as/ folder. The file structure in src/as/ matches with the same file structure as in the Three.js src/ folder.

The gist of the development process is:

  • choose a file from the Three.js repo
  • copy it into the same structure as the Three.js repo, with a .ts extension, and in src/as/ instead of just src/ (someone may have already pasted the Three.js file in our repo, check for that)
  • copy the test associated with the file from the Three.js repo, but with a .spec.ts extension, and place it as a sibling to the source file
  • port the code for the source and tests of the chosen file. You can take the type definitions from the Three.js .d.ts files which are sibling to the .js files, and use those to help create the merged TypeScript (AssemblyScript) code.
  • after porting source and test code, run npm test to test it

For example, when we ported the Object3D class from src/core/Object3D.js in the Three.js repo, we placed it in our repo as src/as/core/Object3D.ts, inside of src/as/ instead of just src/ in order to distinguish the AssemblyScript code from regular TypeScript code in src/ts/. We took src/core/Object3D.d.ts in the Three.js repo as a starting point for the types, merging the types into the same file.

Basically we merged both src/core/Object3D.js and src/core/Object3D.d.ts from the Three.js repo into a single src/as/core/Object3D.ts file in our repo.

Then we ported test/unit/src/core/Object3D.tests.js from the Three.js repo and made it a sibling file of our Object3D.ts file, at src/as/core/Object3D.spec.ts. We prefer to co-locate test files as siblings of the source files, making them easier to navigate.

As you can tell from comparing the test file from Three.js and the test file in glas, porting test code involves converting from QUnit-style tests to using as-pect-style describe, test, and expect functions. See as-pect documentation for more details on available APIs; basically as-pect's API similar to Mocha.js+Chai or Jasmine.

About

WebGL in WebAssembly with AssemblyScript

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 62.6%
  • JavaScript 37.4%