#Design Goals

#Start Simple, but Give Control to Those Who Need It

New users should be able to get started as soon as possible, without having to understand complex concepts. But this comes at the risk of having limited control over what's possible. Renda should initially be built from low-level concepts for advanced users. But it should build its own high-level concepts on top of those.

A good example of this is the build system. While many competing engines take complete control over the build pipeline, with Renda you can choose to let Studio handle bundling or to set up your own pipeline.

Renda can generate boilerplate code for setting up things like the renderer or asset loader, but you are able to write your own implementation for this if you wish.

#Keep Feedback Loops Tight

When working with visual tools, having to wait before seeing your changes applied can be very cumbersome. Ideally, you'd want to see the effects of a change immediately. Changing shader code from a text editor should update shaders right away. There should be no apply or save buttons, every change should be applied and saved immediately.

Ideally, these changes are immediately visible across devices as well, allowing you to collaborate without having to worry that your edits conflict with those of another user.

This translates to making changes while your application is running too. You should be able to change the position of an entity while your application is running, and the effect should be immediately visible both in the entity editor as well as your application. These changes should persist even after the application is stopped.

#Keep It Uniform

To make Renda as easy to learn for new users as possible, the number of new concepts should be kept to a minimum. Instead, with every new feature, an existing concept should be used where possible. This is especially true for concepts that will be used on a daily basis.

For example:

  • Everything is an asset, and assets can be bundled, loaded in the editor, or loaded in a built application. Reusing the same logic for all files means users know what to expect, and we won't have to write many custom implementations.
  • Everything is a window, there are no popups, no toolbar, and no status bar. All the required UI exists within the window related to it.
  • Everything is a treeview, using the same treeview GUI everywhere allows us to quickly convert user-provided data into structured JSON.
  • Everything is a task, when files need to be generated, whether they are a build of a project or baked light maps, the computations should be initiated from tasks.

#Keep Project Files Clean

When the majority of your projects consist of only code, it is easy to figure out what something does since you probably wrote it yourself.

But when the files are generated by an application, in this case Renda Studio, it can be difficult to decipher the contents of a file. So it's important to keep project files as small as possible. This is achieved by only storing variables that have been modified. And if a user resets a variable back to the default value from within Studio, its entry will be removed from the file again.

This makes it easy to figure out what changed across commits, or even what a file does when someone sends it to you.

#Performance Where It Matters

Renda tries to make it as easy as possible to quickly prototype new features. For example, there are many ways to multiply two vectors:

  • Vec3.multiply(vectorA, vectorB) creates a new vector instance.
  • vectorA.multiply(vectorB) applies the result to vectorA.
  • vectorA.multiply(1,2,3) multiplies a new Vec3(1,2,3) with vectorA.
  • vectorA.multiply([1,2,3]) multiplies with an array.
  • vectorA.multiply(matrixB) multiplies with a matrix.

You could argue that allowing this many options is bad for performance, and you'd be right! But the majority of the time you just want to get something working quickly and you don't care that much about performance yet. And even if you did, you'd likely find that the bottleneck lies somewhere else. You might not perform enough caching for instance, or maybe you have loaded a large level even though only a small portion of it is visible.

That said, if a design decision turns out to affect performance significantly, then an option should be made available that is more performant. In the case of a.multiply(b), you can also call a.multiplyVector(b) which performs the same computation but only supports a vector as an argument.

#Keep Bundle Sizes Small

Rendering engines can quickly grow into many lines of code. But you might not always need every feature that an engine has to offer. Usually, tree shaking can get you a long way there, but this might not always be enough.

That's why Renda provides 'engine defines'. By changing these flags, you can selectively remove unnecessary engine components to reduce bundle size.

Let's take a look at the WebGPU renderer for example. The render() function looks something like this:

export class WebGPURenderer {
	render() {
		// ... some setup

		clusterComputeManager.computeLightIndices();

		// ... some more rendering code
	}
}

This computeLightIndices call contains a lot of code. But what if your game is completely unlit? There's now a whole bunch of code that will never be used. Tree shaking isn't going to be of any help either, because the clusterComputeManager gets imported regardless of whether you need it or not.

Therefore the call has been wrapped inside an if statement:

render() {
	// ... some setup

	if (ENABLE_WEBGPU_CLUSTERED_LIGHTS) {
		clusterComputeManager.computeLightIndices();
	}

	// ... some more rendering code
}

That way bundlers will completely remove this code when ENABLE_WEBGPU_CLUSTERED_LIGHTS is false, and as a result, the clusterComputeManager import will be tree shaken.

Ideally, engine defines are automatically set to the appropriate value by analyzing the assets that are being used.