A Java Framework for building real time graphics applications. This is my final project of the Software Engineering degree at ULPGC.
- Introduction
- What you can create
- Performance
- Framework architecture
- Getting Started
- Configuration
- How to include it in your project
Beryl is a framework purely written in Java to develop high performance graphics applications. It is designed to run 3D desktop games and simulations, but maybe I add mobile and web support in the future.
I have been learning a lot of things about graphics programming during this project, implementing features as I was learning them. My main motivations to create Beryl was:
- The fact that Java has a huge potential to create this kind of programs. Despite it is generally slower than C/C++, it can equal their performance or even beat them in some situations. Plus, Java is multiplatform and is an easier language to work with, because the programmer does not have to deal with manual memory management, for example.
- The research of 3D batch-rendering techniques to improve performance.
- Use concurrency to update scenes and generating rendering commands in multiple threads.
- Make it easy and straightforward to develop with.
- Design Beryl to support multiple graphics APIs in the future, especially Vulkan.
To achieve this, I worked with modern OpenGL techniques, following the AZDO philosophy (Approaching Zero Drive Overhead), that is, reducing the driver's work as much as possible, while putting much more responsibility on the application code. In other words, using OpenGL like Vulkan.
Beryl uses LWJGL3 as its backend, a great library that offers Java bindings to C libraries (like OpenGL, GLFW or Vulkan) and allows easy off-heap memory management.
Beryl supports the following features for now:
- Multiple light sources
- Cascaded shadow maps (only directional lights)
- Terrain generation with heightmaps
- Water
- 3D sound
- Dynamic skybox (HDR or traditional 6-image sets)
- Fog
- 3D model loading
- Blinn-Phong
- PBR metallic-roughness workflow
- Customizable configuration settings
All of these functionalities are going to be continually improved in the future, as well as adding many others.
You can create quite different 3D scenes with Beryl very easily:
You can develop outdoor 3D environments very easily with Beryl, like, for example, a beautiful forest:
Forest daytime, 1000 trees, water and shadows
Maybe you prefer adventuring into the woods at night with a flashlight:
Forest at night, player porting a flashlight
You can't imagine how creepy becomes a forest with some fog around...
Forest with dense fog
You can also render interiors, like your dream bedroom:
Room scene, rendering with PBR
Using 2 textures and the sun light, you can simulate the Earth rotation and see the lights on the dark side!
Earth, light side
Earth, dark side
Create incredible scenes with physically accurate algorithms and HDR environments to make the scene feel real.
Different PBR materials, rendered with IBL
Rusted iron reflecting night lights
PBR Model with metallic workflow: the amazing Cerberus model
Beryl uses bindless textures, frustum culling and multithreading drawing command generation with indirect rendering. This means that multiple objects are drawn at once, boosting the performance up to 400% faster in some scenes.
Stress Test. 50000 cubes are drawn, all of them rotating each frame, and 1000 cubes are getting destroyed and created every 100 ms. I get 105 FPS on GTX 970
The framework contains individual, single responsibility systems, that can be divided in 3 different levels:
Beryl Systems
Each system can depend on other systems in the same or lower levels, but never on higher layers. This makes a hierarchical architecture that correctly defines the order in which these systems have to be initialized and terminated.
Beryl uses a scene system to update and render your worlds. A scene contains entities, which are the basic world objects. An entity is nothing by itself, but they can contain multiple components. A component can be just data, may define a behaviour, or both. With this system, you get rid of complex inheritance hierarchies, so the application is easier to scale.
An entity only exists within its scene. Entities can only be in 1 scene, and a component must be in only 1 entity. In addition, an entity can only contain 1 component of a specific class, but can contain multiple components of the same type.
As you can see, the component class and the component type are different things. The class of a component refers to its Java class, i.e. UpdateMutableBehaviour. However, the type is usually the base class of that kind of components, i.e. AbstractBehaviour.
So, you cannot add 2 UpdateMutableBehaviour components in the same entity, but it may contain 100 different implementations of AbstractBehaviour.
Simple class diagram of the Beryl Scene Entity-Component System
Besides, the components are group by type, which are controlled by component managers, one for each type. This divides the
responsibility in multiple classes, so it is more maintainable, readable and faster. Faster because the scene does not have to
traverse huge and complex graphs to search and update or render the required components, but update lists of components of one type.
This lists, which are implemented as arrays, are super fast to iterate, can improve cache locality and most important, allows parallel
processing of each component.
Developing with Beryl is very easy and straightforward. First of all, you need to extend the abstract class BerylApplication:
public class MyGame extends BerylApplication {
@Override
public void onStart(Scene scene) {
// Cool things here
}
}
When you inherit from BerylApplication, you must implement the method onStart. It will give you a Scene instance, so you can start building up your world!
For example, lets make a simple example of a cube rotating each frame, a directional light and a skybox:
@Override
public void onStart(Scene scene) {
// First of all, position the camera so we can see the cube.
scene.camera().position(0, 0, 20);
// Create the cube entity. An entity may have a name, and it must be unique.
Entity cube = scene.newEntity("The Cube");
// Add a Transform. It defines the position, scale and rotation of an object in the scene.
// A Transform is necessary in visible objects, but not in invisible ones, like in game controllers for example.
// Lets set this cube at position (0, 0, 0) and scale it by 2.
cube.add(Transform.class).position(0, 0, 0).scale(2.0f);
// Now attach a mesh view to it to actually see it on the scene.
// Common meshes, like cube, sphere or quad, are already loaded and ready to use.
StaticMesh cubeMesh = StaticMesh.cube();
// Now we need a material. Lets create a material with a green color.
// Materials have a unique name, and are created through a factory.
Material material = PhongMaterial.getFactory().getMaterial("My Material", mat -> {
// If the material named "My Material" has not been created, then this piece of code will be executed.
// Here you can initialize the material. The materials are mutable, so you can change their properties at runtime.
mat.color(Color.colorGreen()).shininess(32.0f);
});
// Create a StaticMeshInstance component and attach a new StaticMeshView to it.
// A mesh view simply binds a mesh with a material. A mesh instance is a component that can have multiple mesh views.
cube.add(StaticMeshInstance.class).meshView(new StaticMeshView(cubeMesh, material));
// Now, lets add a small behaviour script that rotates the cube each frame.
// UpdateMutableBehaviour is a special type of behaviour component that can change its onUpdate method at runtime.
cube.add(UpdateMutableBehaviour.class).onUpdate(self -> {
// This will be executed each frame. All onUpdate methods are invoked at the same time, in parallel.
// If you are going to perform tasks that should be executed in a single thread, use onLateUpdate instead.
// "Self" represents this behaviour component. You can get the components of its entity with the "get" method:
Transform transform = self.get(Transform.class);
transform.rotateY(Time.seconds());
});
// Now, lets add the directional light
scene.environment().lighting().directionalLight(new DirectionalLight().direction(0, 0, 1));
// And finally, set the skybox
scene.environment().skybox(SkyboxFactory.newSkyboxHDR("Path-to-skybox.hdr"));
}
}
Awesome! You have built your first application with Beryl! Now, its time to actually launch it:
public class MyGame extends BerylApplication {
public static void main(String[] args) {
// Simply call Beryl's launch method with an instance of your BerylApplication class.
Beryl.launch(new MyGame());
}
@Override
public void onStart(Scene scene) {
// ...
}
}
You can customize many aspects of Beryl execution, like debugging, enable asserts, MSAA, logging parameters, and much more.
These configuration parameters are stored in static final (immutable) variables when Beryl launches, so you can only modify them before starting Beryl.
To modify the configuration settings, use the BerylConfiguration variables:
public class MyGame extends BerylApplication {
public static void main(String[] args) {
// Customize configuration before Beryl launches:
BerylConfiguration.MSAA_SAMPLES.set(8);
BerylConfiguration.SCENE_SHADING_MODEL.set(ShadingModel.PHONG);
BerylConfiguration.FIRST_SCENE_NAME.set("My awesome Beryl game");
// BerylConfigurationHelper contains useful methods to set predefined sets of configurations.
BerylConfigurationHelper.debugConfiguration();
// Finally, launch your application.
Beryl.launch(new MyGame());
}
@Override
public void onStart(Scene scene) {
// ...
}
}
You can download the jar files in the releases section of this repository.
To include Beryl as a Maven dependency, simply copy and paste this:
<dependency>
<groupId>naitsirc98</groupId>
<artifactId>beryl</artifactId>
<version>0.1-snapshot</version>
</dependency>
Check out the packages section of this repository to see all the versions available.