WebGL project using ThreeJS, HTML5 and OOJS (object oriented javasctipt) for exploring several computer-graphics techniques:
- Geometry and Normals calculation for complex models
- UV coordinates calculation
- Surface Smoothing by using Vertices Normals
- Lighting and Shadows
- Skybox and reflections
- User Interface (sliders, toggles, buttons)
- Finite State Machine to handle "shooting robot"
- Collision detection using AABB
- Particles System with Nebula-Threejs
NOTE: You can play this app at smooth-dragon-github-page (desktop or mobile device)
- three-global.js
- It is used to create a global object
THREEand add functionalities to it- Imports
three.jslibrary as module from npm - Names it
THREE(following the nomenclature used in other modules) - Exports the object
THREEfrom this script / module to be imported into the rest (ex: in OrbitControls.js) - In OrbitControls.js, or other scripts, new functionality is added to
THREEobject
- Imports
- It is used to create a global object
- Graphical Programming with Threejs libgptjs
- The classes at
libgptjs/are importingthree-global.js - Wrapper / Library to facilitate re-use of code and organize the graphics pipeline
- It contains several objects (classes) for wrapping all the logic required for creating an scene with threejs
- This allows modularity and we can reuse code creating instances of those clases
- The classes at
- GPT_Coords
- Gets vertices (Float32Array) and edges array (Uint32Array)
- Calculates the normal vector for each triangle
- Provides a method for calculating the UV coordinates for each triangle
- GPT_Model
- Simple class to integrate mesh + geometry + material
- Provides method for cleaning gl buffers that were reserved
- GPT_LinkedModel
- Model formed of joining several
THREE.Object3Din order to create articulated models like robot arms - Provides method for adding a new link between two Object3D and finally linking all of them in sequence
- Model formed of joining several
- GPT_ModelCollider
- Attaches an AABB (axis aligned bounding box) to an existign Mesh
- Provides a method for detecting collision with another AABB
- GPT_Scene
- List of
GPT_Models andGPT_Light - Provides abstract methods for initial configuration and updates in every frame
- These methods have to be overriden when creating the instance of the
GPT_Scene
- These methods have to be overriden when creating the instance of the
- Provides methods for adding and removing models at runtime
- List of
- GPT_Render
- It initializes the camera and camera-handler
- This is the main object that creates a
webgl-rendererand invokes methods ofGPT_Scene
- GPT_App
- Top-level object that configures the
windowand usesGPT_Render - It contains the main loop for animation in which the
updateandrenderare being invoked
- Top-level object that configures the
- main.js
- It is the entry point. It checks webgl compatibility and instanciates
GPT_Renderer,GPT_App, andSceneDragon - Then invokes init and run loop
- It is the entry point. It checks webgl compatibility and instanciates
- Common.js
- Contains all constants to be re-used in several points in the code
- CoordsDragon.js
- Stores arrays of dragon model (vertices and edges)
- Since it inherits from
GPT_Coordsit provides methods for computing normals and UVs coordinates
- CoordsGripper.js
- Stores arrays of gripper model (vertices and edges)
- Since it inherits from
GPT_Coordsit provides methods for computing normals and UVs coordinates
- ModelSkybox.js
- Creates a big cube and maps the texture to simulate environment
- These skybox images will be reflected on the Dragon surface and Gripper surface
- ModelDragon.js
- Inherits from
GPT_Modeland overridesget_geometryandget_materialmethods - Creates and initializes
geometryandmaterialobjects to be inserted into amesh - Computes
UVcoordinates per face (triangle) in order to simulate reflections of the skybox onto the dragon surface - Contains a
GPT_ModelCollider
- Inherits from
- ModelGripper.js
- Idem to ModelDragon
- ModelRobot.js
- Inherits from
GPT_LinkedModel - Creates separately the parts of the robot (base, arm, forearm, hand and gripper). Then links them all in sequence
- Inherits from
- ModelTrajectory.js
- Given 2 initial points to be used as direction vector
- It computes the control points (
p1, p2, p3, peak and end) to be used later into the spline points calculation - Control points form a triangle with one of the edges following the
p1andp2directionPeakpoint is in the middle of triangle and is the highest pointEndpoint is on the floor
- Spline points are calculated using the control points and
catmullromwith N (30) segments - Final spline points are used to create line geometry to be rendered
- It computes the control points (
- Given 2 initial points to be used as direction vector
- ModelBullet.js
- Creates the geometry, material, mesh, and GPT_ModelCollider
- Needs a trajectory and a starting point3D
- Provides a method for moving the bullet between 2 consecutive points3D of the trajectory based on time passed since last frame
- InputManager.js
- Checks if it is running on mobile device or desktop
- Creates the UI (sliders, toggles, etc.) and installs the
onChangecallbacks to be executed when a value is updated by the user - Creates html button for "shoot" and attaches the corresponding callback
- FSM_Robot.js
- Defines a finite state machine for robot shooter
- Defines
States,EventsandTransitions - Defines Transitions as a dictionary of allowed state-event pairs
- Provides methods for
transitingfrom one state to other depending on the "Event" - Provides method for
updatingthe current state based on timers expiration
- SceneDragon.js
- Contains the handling of main interactions: InputManager, animation (update) of objects, etc.
- Inherits from
GPT_Sceneand overridescreateObjects,createLights,updateObjectsandupdateLightsmethods - Performs all setting up of models and lights: floor, dragon, skybox, robot, trajectory, etc.
- Performs periodic updates of models and lights: translate, rotate, destroy and create new trajectory, etc.
- Contains a method where actions are triggered depending on the change of state of
FSM_Robot- Any change of state is reflected into the UI
Idle- Rotate Dragon, update AABB
loading_bullet- Rotate shooting arm
- Increase power while user keeps clicking and update UI slider
- When power increased set shooting arm to red color
bullet_traveling- Draw the trajectory
- Move the bullet along the trajectory
- Rotate while traveling
hit- Blink dragon to red
- Stop bullet at collision point
- Limits the reaction to the incoming "shoot events" by checking if current robot state is
idle
- A triangle is the basic polygone
- It is formed of 3 vertices, each vertex has 3 components float (x, y, z)
- Its vertices are defined clockwise by default. This is taken into account when computing the face (triangle) normal vector
- A
geometryin threejs is formed of several arraysposition- It is a
Float32Arrayin which all the coordinates of all vertices of all triangles are packed together - Array lenght is
3 * num vertices - Example forming the first 2 triangles
CoordsGripper.prototype.getArrayVertices = function () { return new Float32Array([ 0, 0, 0, 0, 20, 0, 19, 20, 0, 19, 0, 0, 0, 20, 4, 0, 0, 4,
itemSize3 because there are 3 components per vertexModelDragon.prototype.get_geometry = function () { const _geom = new THREE.BufferGeometry(); _geom.setAttribute( "position", new THREE.BufferAttribute(this.coords.vertices_coordinates, 3) );
- It is a
normal- It is a
Float32Arraycontaining all the normal vectors for all triangles - Array lenght is
3 * num triangles - It is computed at GPT_Coords
calculateNormals - Idem to
positions
- It is a
indices- It is a
UInt32Arraycontaining all the sequence of indices (ofpositionsarray) to form triangles - Example forming the first 2 triangles
CoordsGripper.prototype.getArrayEdges = function () { return new Uint32Array([ 2, 0, 1, 3, 0, 2,
itemSize1 because there are 1 component per vertex-index_geom.setIndex(new THREE.BufferAttribute(this.coords.edges_indices, 1));
- It is a
uv- It is a
Float32Arraycontaining the UV coordinates for all vertices of all triangles - Each vertex will have 2 UV components (texture coordinates)
- UV coordinate values are in range [0.0, 1.0]
- Array lenght is
6 * num triangles - It is computed at GPT_Coords.js
getUVs itemSize2 because each vertex has 2 UV componets_geom.setAttribute( "uv", new THREE.BufferAttribute(uvs, 2) );
- It is a
- A
Mesh Phong Materialin threejs is needed to define the rendering of the geometryModelDragon.prototype.get_material = function () { // loading TextureCube as skybox const _mat = new THREE.MeshPhongMaterial( { color: 0xe5ffe5, emissive: 0xb4ef3e, flatShading: true, // initially per-triangle normals specular: 0x003300, shininess: 70, side: THREE.FrontSide, transparent: true, opacity: 0.75, envMap: Common.SKYBOX_CUBE_TEXTURE } );
- A mesh in threejs is formed of a geometry and a material
this.mesh = new THREE.Mesh(this.geometry, this.material);
GPT_Coords.js getUVs
- Calculates UV for planar surface (x, y, z) where z = 0
- Depends on geometry bounding box
- Computes the UV values for each face (triangle)
- Stores UV coordinates for each triangle (6 float components)
- First you need to have per-face (triangle) normals
- GPT_Coords.js
calculateNormals- Creates
points3Darray by grouping 3 values frompositionsarray - Creates triagles array by grouping 3 values from
points3Darray - Computes normals for each triangle clockwise
- v1 = p2 - p1
- v2 = p3 - p2
- cross_product(v1, v2)
- Applies modulus
- Stores normal (3 float components)
- Creates
- GPT_Coords.js
- Then you can invoke
computeVertexNormalsin order to make the transition between faces (triangles) smoother when computing the lightingSceneDragon.prototype.createDragon = function () { // pre-calculated for surface smoothing this.dragon_model.geometry.computeVertexNormals();
- You must update the material to be
smooth shading(flatShading= false)_cbs.on_change_dragon_smoothing = (new_val_) => { const _dragon = this.gpt_models.get("dragon"); // boolean if (new_val_) { _dragon.material.flatShading = false; } else { _dragon.material.flatShading = true; } _dragon.material.needsUpdate = true; };
SceneDragon.js createLights
- Creates an
ambient lightthat will be added when shading the models surface- 5% white
- It doesn't need a position into the Scene
- Creates a
point light- 75% white
- Emits in all directions
- Creates a
directional light- 75% white
- Emits only in the direction vector provided (-200, 200, 0)
- Creates a
focal light- 75% white
- Emits light in a cone volume
- Direction of the central lighting vector is pointing to the center of the floor (0,0,0)
- Defines
angleanddistanceto make afading lightingfrom the center of the cone to the exterior - Defines the cone like shape by defining parameters of shador:
near,farandfov
- ModelSkyBox.js
- Creates a
BoxGeometry - Attaches a texture (material) per face
- The texture images must be specifically for a cube texture, like the ones at skybox_images/
- They need to be mapped properly ordered
posx, negx, posy, negy, posz, negz
- Makes the inner of the box visible instead of the outside
ModelSkybox.prototype.get_material = function () { ... _cubeFacesMaterials.push( new THREE.MeshBasicMaterial({ map: _loader.load(_img_path), color: 0xffffff, // white side: THREE.BackSide // inside the cube }) ...
- Creates a
- Reflections on ModelDragon.js
- Sets
transparent,opacity, andshininesto simulate "glass" - Sets the
Skybox Textures ArrayasenvMap
ModelDragon.prototype.get_material = function () { const _mat = new THREE.MeshPhongMaterial( { color: 0xe5ffe5, emissive: 0xb4ef3e, flatShading: true, // initially per-triangle normals specular: 0x003300, shininess: 70, side: THREE.FrontSide, transparent: true, opacity: 0.75, envMap: Common.SKYBOX_CUBE_TEXTURE } );
- Sets
- Idem for hand of the robot ModelGripper.js
- It creates a
dat.guiobject- dat.gui assumes the GUI type based on the target's initial value type:
- boolean:
checkbox - int / float:
slider - string:
text input - function:
button
- boolean:
- When user updates a value with the UI we store the new value in
effectvariables
- dat.gui assumes the GUI type based on the target's initial value type:
- It attaches the corresponding
onChangecallbacks to be executed when a new value is set using the UI - Saves references to UI controllers, so we can reflect updates on the UI
- Reflects text of
robot_state - Reflects value of
robot_power
- Reflects text of
- Creates a custom html button for
shoot, which is separated from the rest of the panel for better usability - Creates a
statswidget at the bottom of the canvas container. This reflects the frames per second
- Defines
States,Eventsand allowedTransitions - A transition is defined as
destination stategiven a pairState-Event - Updates the current state based on the expiration of timers
IDLE--> shoot -->LOADING_BULLETLOADING_BULLET--> timer.expired -->BULLET_TRAVELINGBULLET_TRAVELING--> collision -->HITBULLET_TRAVELING--> timer.expired -->NO_HITHIT/NO_HIT--> timer.expired -->IDLE
- Main concept can be read at link
- Attaches an AABB (axis aligned bounding box)
- Updates the dimensions of the AABB at runtime
- Checks if intersects with other AABB (collided)
- Follow tutorial at three-nebula.org
- Nebula is a particle system engine that works with threejs
- It provides an editor to create manually and save to json file
- Adapted manually for our SceneDragon scale:
{ "type": "Radius", "properties": { "width": 20, "height": 80, "isEnabled": true }, { "type": "RadialVelocity", "properties": { "radius": 400, "x": 0, "y": 0, "z": 1, "theta": 10, "isEnabled": true } - The rest of values (color, sprite, life cycle, etc.) were edited using the Nebula editor (windows)
- Loads the particle system from json file and creates an instance of
nebulathat will be used to renderNebula.SpriteRendererneeds the mainTHREE.Scene
- Provides a method for updating the
dragon fire particlesaccording to dragon mouth positionDragonFire.update_to_dragon_mouthperforms a sequence of translations and rotations to place / update properly the particles emitter while dragon is rotating
This project is buildt with NodeJS. The dependencies packages and configuration are locate at package.json
- Working with versions:
- npm:
6.14.17 - nodejs:
v16.15.0
- npm:
- Install all dependencies
npm install
- Two modes of "compiling" the code:
devandbuild- Running in development mode with a local webpack-dev-server
npm run dev
- Building compressed / production code for deployment in a remote server
npm run build- Assets (images, index, etc.) and code will be placed at
./dist/ - You can use vs-code-plugin live-server to simulate a remote server and view the result
- Running in development mode with a local webpack-dev-server
This project uses webpack-5 for building the final js code. Webpack configuration is done at config/
- paths.js
src- Directory path for source files path (libgptjs, main scripts)
build- Directory path for production built files (compressed, images, html, etc.)
static- Directory path from which the assets will be copied to the build folder
- webpack.common.js
- Uses webpack-plugins to integrate all resources (js scripts, html, images, etc.)
entry- Defines the point as index.js, which also loads index.scss and main.js
- This makes canvas background green and starts our app entry point (main.js)
- Defines the point as index.js, which also loads index.scss and main.js
output- Defines the final js code bundle
[name].bundle.jswhich will be placed atbuild
- Defines the final js code bundle
CopyWebpackPlugin- Used to copy resources from origin to destination assets folders
HtmlWebpackPlugin- Used to load init_template.html, replaces some headers and defines the div where our project will be embedded into:
div id="container"></div>
- Used to load init_template.html, replaces some headers and defines the div where our project will be embedded into:
- webpack.dev.js
- Includes
webpack.common.jsand adds configuraiton for development server
- Includes
- webpack.prod.js
- Includes
webpack.common.jsand adds configuration for building production bundle (split in chunks, minify code, etc.)
- Includes
git clone https://github.com/GiovannyJTT/Smooth-Dragon.git
cd Smooth-Dragon
npm install # install all nodejs packages needed for this project (in node_modules/ folder)
npm run dev # compile and run a development version
npm run build # build an optimized website (html + javscript + images) in dist/ folderFor reducing the transmision of data when loading our webgl app in the client web browser we can build a minified version of ThreeJS. This will compress and unify all the ThreeJS scripts in one.
-
Tutorial for building Threejs.min using google closure compiler: Link
-
Using an online (unofficial) builder: Link
Summary (building Threejs.min using google closure compiler):
git clone --depth=30 https://github.com/mrdoob/three.js.git
cd ./three.js
npm install # This will install google closure compiler dependencies (Youy need to install NodeJS for npm)
npm run build-closure
# "created build/three.module.js in 2.3s"
:"
ls -lh build/
total 3,0M
-rw-r--r-- 1 Gio 197121 1,2M jul. 8 16:01 three.js
-rw-r--r-- 1 Gio 197121 614K jul. 8 16:01 three.min.js
-rw-r--r-- 1 Gio 197121 1,2M jul. 8 16:01 three.module.js
"