├── .nvmrc ├── .env ├── src ├── App.css ├── webgl-app │ ├── scenes │ │ ├── camera-transitions │ │ │ ├── assets.js │ │ │ ├── data │ │ │ │ ├── dolly-data-0.json │ │ │ │ └── dolly-data-1.json │ │ │ └── camera-transitions-scene.js │ │ ├── interactive-sphere │ │ │ ├── assets.js │ │ │ ├── objects │ │ │ │ └── sphere │ │ │ │ │ ├── shader.glsl.js │ │ │ │ │ └── sphere.js │ │ │ └── interactive-sphere-scene.js │ │ ├── landing │ │ │ ├── assets.js │ │ │ ├── objects │ │ │ │ ├── jam3 │ │ │ │ │ ├── shader.glsl.js │ │ │ │ │ └── jam3.js │ │ │ │ └── particles │ │ │ │ │ ├── shader.glsl.js │ │ │ │ │ ├── particles.js │ │ │ │ │ └── particles-normal.js │ │ │ └── landing-scene.js │ │ ├── empty │ │ │ └── empty-scene.js │ │ ├── preloader │ │ │ └── preloader-scene.js │ │ └── base │ │ │ └── base-scene.js │ ├── shaders │ │ └── math.glsl.js │ ├── utils │ │ ├── math.js │ │ ├── canvas.js │ │ ├── geometry.js │ │ ├── stats.js │ │ ├── dispose-objects.js │ │ ├── query-params.js │ │ ├── gui.js │ │ ├── material-modifier.js │ │ ├── render-target-helper.js │ │ ├── render-stats.js │ │ └── screenshot.js │ ├── app-state.js │ ├── loading │ │ ├── loaders │ │ │ ├── loader.js │ │ │ ├── image-loader.js │ │ │ ├── three-fbx-loader.js │ │ │ ├── three-texture-loader.js │ │ │ ├── json-loader.js │ │ │ ├── three-gltf-loader.js │ │ │ └── group-loader.js │ │ ├── asset.js │ │ ├── asset-loader.js │ │ └── asset-manager.js │ ├── rendering │ │ ├── render-target.js │ │ ├── post-processing │ │ │ ├── passes │ │ │ │ ├── transition-pass │ │ │ │ │ ├── shader.glsl.js │ │ │ │ │ └── transition-pass.js │ │ │ │ ├── final-pass │ │ │ │ │ ├── shader.glsl.js │ │ │ │ │ └── final-pass.js │ │ │ │ └── film.glsl.js │ │ │ └── post-processing.js │ │ ├── renderer.js │ │ ├── preload-gpu.js │ │ ├── resize.js │ │ └── graphics.js │ ├── objects │ │ └── background │ │ │ ├── shader.glsl.js │ │ │ └── background.js │ ├── settings.js │ ├── lights │ │ ├── ambient.js │ │ ├── directional.js │ │ ├── point.js │ │ └── spot.js │ ├── cameras │ │ ├── cameras.js │ │ └── camera-dolly │ │ │ ├── camera-dolly-manager.js │ │ │ └── camera-dolly.js │ ├── interaction │ │ ├── interactive-object.js │ │ └── touch-controls.js │ └── webgl-app.js ├── assets │ ├── test │ │ ├── cube.fbx │ │ ├── scene.fbx │ │ ├── test.jpg │ │ └── config.json │ └── landing │ │ ├── config.json │ │ └── jam3.fbx ├── index.js ├── index.css └── App.js ├── .prettierignore ├── .env.gh-pages ├── public ├── assets │ ├── webgl │ │ ├── test │ │ │ ├── test.json │ │ │ ├── cube.fbx │ │ │ ├── scene.glb │ │ │ └── test-128.jpg │ │ └── landing │ │ │ └── jam3.glb │ └── lib │ │ └── draco │ │ └── gltf │ │ └── draco_decoder.wasm ├── robots.txt ├── favicon.ico ├── logo180.png ├── logo256.png ├── manifest.json └── index.html ├── .prettierrc ├── webgl-react-app.gif ├── scripts └── assets │ ├── config.js │ ├── optimise.js │ ├── model-optimiser.js │ └── texture-optimiser.js ├── AUTHORS ├── .editorconfig ├── .flowconfig ├── .gitignore ├── CHANGELOG.md ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── LICENSE ├── .eslintrc.json ├── CONTRIBUTING.md ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v13.7.0 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=/ 2 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | } 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.env.gh-pages: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=/webgl-react-boilerplate 2 | -------------------------------------------------------------------------------- /public/assets/webgl/test/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "b" 3 | } 4 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/camera-transitions/assets.js: -------------------------------------------------------------------------------- 1 | export default []; 2 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/interactive-sphere/assets.js: -------------------------------------------------------------------------------- 1 | export default []; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120 4 | } 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/public/logo180.png -------------------------------------------------------------------------------- /public/logo256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/public/logo256.png -------------------------------------------------------------------------------- /webgl-react-app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/webgl-react-app.gif -------------------------------------------------------------------------------- /src/assets/test/cube.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/src/assets/test/cube.fbx -------------------------------------------------------------------------------- /src/assets/test/scene.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/src/assets/test/scene.fbx -------------------------------------------------------------------------------- /src/assets/test/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/src/assets/test/test.jpg -------------------------------------------------------------------------------- /src/assets/landing/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": {}, 3 | "models": { 4 | "jam3.fbx": { "convert": true } 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/landing/jam3.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/src/assets/landing/jam3.fbx -------------------------------------------------------------------------------- /public/assets/webgl/test/cube.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/public/assets/webgl/test/cube.fbx -------------------------------------------------------------------------------- /public/assets/webgl/landing/jam3.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/public/assets/webgl/landing/jam3.glb -------------------------------------------------------------------------------- /public/assets/webgl/test/scene.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/public/assets/webgl/test/scene.glb -------------------------------------------------------------------------------- /public/assets/webgl/test/test-128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/public/assets/webgl/test/test-128.jpg -------------------------------------------------------------------------------- /public/assets/lib/draco/gltf/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/webgl-react-boilerplate/HEAD/public/assets/lib/draco/gltf/draco_decoder.wasm -------------------------------------------------------------------------------- /scripts/assets/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | textures: { 3 | resize: false, 4 | sizes: [] 5 | }, 6 | models: { 7 | convert: false 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | const root = document.getElementById('root'); 7 | if (root) ReactDOM.render(, root); 8 | -------------------------------------------------------------------------------- /src/assets/test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": { 3 | "test.jpg": { "resize": true, "sizes": [128] } 4 | }, 5 | "models": { 6 | "scene.fbx": { "convert": true }, 7 | "cube.fbx": { "convert": false } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This file contains a list of all those who have helped contribute to this project <3 2 | # Thank you all! 3 | 4 | Amelie Maia Rosser 5 | Iran Reyes 6 | Peter Altamirano 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | end_of_line = lf 11 | max_line_length = 120 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/landing/assets.js: -------------------------------------------------------------------------------- 1 | import Asset from '../../loading/asset'; 2 | import Loader from '../../loading/loaders/loader'; 3 | import settings from '../../settings'; 4 | 5 | export default [ 6 | new Asset({ 7 | id: 'jam3-logo', 8 | src: `${settings.baseUrl}/assets/webgl/landing/jam3.glb`, 9 | type: Loader.threeGLTF 10 | }) 11 | ]; 12 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/* 3 | .*/flow-typed/.* 4 | /build/.* 5 | /public/.* 6 | /scripts/.* 7 | 8 | [include] 9 | /src/.* 10 | 11 | [libs] 12 | 13 | [lints] 14 | 15 | [options] 16 | all=true 17 | emoji=true 18 | esproposal.optional_chaining=enable 19 | 20 | [strict] 21 | 22 | [version] 23 | 0.118.0 24 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/empty/empty-scene.js: -------------------------------------------------------------------------------- 1 | import BaseScene from '../base/base-scene'; 2 | import { VECTOR_ZERO } from '../../utils/math'; 3 | 4 | export default class EmptyScene extends BaseScene { 5 | constructor(id: string, clearColor: number) { 6 | super({ id, clearColor }); 7 | this.camera.position.set(0, 0, 10); 8 | this.camera.lookAt(VECTOR_ZERO); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/webgl-app/shaders/math.glsl.js: -------------------------------------------------------------------------------- 1 | // PI constant 2 | export const PI = ` 3 | #define PI 3.14159265359 4 | `; 5 | 6 | /* Taken from threejs common.glsl */ 7 | export const rand = ` 8 | highp float rand( const in vec2 uv ) { 9 | const highp float a = 12.9898, b = 78.233, c = 43758.5453; 10 | highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); 11 | return fract(sin(sn) * c); 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | overflow: hidden; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 5 | 'Droid Sans', 'Helvetica Neue', sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .vscode 25 | flow-typed 26 | -------------------------------------------------------------------------------- /src/webgl-app/utils/math.js: -------------------------------------------------------------------------------- 1 | import { Math as Math3, Vector3 } from 'three'; 2 | 3 | // Math constants that are regularly used 4 | export const TWO_PI = Math.PI * 2; 5 | export const PI = Math.PI; 6 | export const HALF_PI = Math.PI / 2; 7 | export const QUARTER_PI = Math.PI / 4; 8 | export const VECTOR_ZERO = new Vector3(); 9 | export const VECTOR_ONE = new Vector3(1, 1, 1); 10 | export const VECTOR_UP = new Vector3(0, 1, 0); 11 | 12 | export default Math3; 13 | -------------------------------------------------------------------------------- /src/webgl-app/utils/canvas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a canvas and 2d context 3 | * 4 | * @export 5 | * @param {Number} width 6 | * @param {Number} height 7 | * @returns 8 | */ 9 | export default function createCanvas(width: number, height: number) { 10 | const canvas = document.createElement('canvas'); 11 | canvas.width = width; 12 | canvas.height = height; 13 | const ctx = canvas.getContext('2d'); 14 | return { 15 | ctx, 16 | canvas 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/webgl-app/app-state.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class to manage the state of the webgl app 3 | * 4 | * @export 5 | * @class AppState 6 | */ 7 | export default class AppState { 8 | ready: boolean; 9 | 10 | constructor(props: Object = {}) { 11 | this.ready = props.ready || false; 12 | } 13 | 14 | equals(state: AppState) { 15 | return this.ready === state.ready; 16 | } 17 | 18 | clone() { 19 | return new AppState({ 20 | ready: this.ready 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## v0.2.0 6 | 7 | ### Added 8 | 9 | - Camera transition scene 10 | - New landing scene 11 | - gh-pages 12 | - Screenshot capture 13 | - Flow typing 14 | 15 | ### Updated 16 | 17 | - Move interactive sphere to new scene 18 | 19 | ### Fixed 20 | 21 | - Husky pre-hooks not working 22 | -------------------------------------------------------------------------------- /src/webgl-app/loading/loaders/loader.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import Asset from '../asset'; 3 | 4 | /** 5 | * Base loader 6 | * 7 | * @class Loader 8 | * @extends {EventEmitter} 9 | */ 10 | class Loader extends EventEmitter { 11 | asset: Asset; 12 | static json: string = 'json'; 13 | static image: string = 'image'; 14 | static threeFBX: string = 'fbx'; 15 | static threeGLTF: string = 'gltf'; 16 | static threeTexture: string = 'texture'; 17 | } 18 | 19 | export default Loader; 20 | -------------------------------------------------------------------------------- /src/webgl-app/loading/asset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 4 | * @interface AssetConfig 5 | */ 6 | export interface AssetConfig { 7 | id: string; 8 | src: string; 9 | type: string; 10 | args?: Object; 11 | data?: mixed; 12 | } 13 | 14 | /** 15 | * 16 | * 17 | * @export 18 | * @class Asset 19 | */ 20 | export default class Asset { 21 | id: string; 22 | src: string; 23 | type: string; 24 | args: Object; 25 | data: mixed; 26 | constructor(config: AssetConfig) { 27 | Object.assign(this, config); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/render-target.js: -------------------------------------------------------------------------------- 1 | import { WebGLRenderTarget, LinearFilter, NearestFilter, RGBFormat, UnsignedByteType } from 'three'; 2 | 3 | export function createRenderTarget(width: number = 1024, height: number = 1024, options: Object = {}) { 4 | const defaults = { 5 | minFilter: LinearFilter, 6 | magFilter: NearestFilter, 7 | format: RGBFormat, 8 | type: UnsignedByteType, 9 | stencilBuffer: false 10 | }; 11 | return new WebGLRenderTarget(width, height, Object.assign({}, defaults, options)); 12 | } 13 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/post-processing/passes/transition-pass/shader.glsl.js: -------------------------------------------------------------------------------- 1 | export const vertexShader = ` 2 | void main() { 3 | gl_Position = vec4(position, 1.0); 4 | } 5 | `; 6 | 7 | export const fragmentShader = ` 8 | uniform sampler2D texture0; 9 | uniform sampler2D texture1; 10 | uniform float transition; 11 | uniform vec2 resolution; 12 | void main() { 13 | vec2 uv = gl_FragCoord.xy / resolution; 14 | vec4 texel0 = texture2D(texture0, uv); 15 | vec4 texel1 = texture2D(texture1, uv); 16 | gl_FragColor = mix(texel0, texel1, transition); 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /src/webgl-app/utils/geometry.js: -------------------------------------------------------------------------------- 1 | import { BufferGeometry, BufferAttribute } from 'three'; 2 | 3 | /** 4 | * Return a triangle that covers screen-space 5 | * Mainly used for post processing 6 | * https://github.com/mikolalysenko/a-big-triangle 7 | * 8 | * @export 9 | * @returns 10 | */ 11 | export function bigTriangle() { 12 | const geometry = new BufferGeometry(); 13 | const attribute = new BufferAttribute(new Float32Array([-1, -1, 0, -1, 4, 0, 4, -1, 0]), 3); 14 | geometry.setAttribute('position', attribute); 15 | geometry.setIndex([0, 2, 1]); 16 | return geometry; 17 | } 18 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/camera-transitions/data/dolly-data-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": 20, 3 | "origin": [ 4 | { 5 | "x": 0, 6 | "y": 0, 7 | "z": 20 8 | }, 9 | { 10 | "x": 0, 11 | "y": 5, 12 | "z": 15 13 | }, 14 | { 15 | "x": 0, 16 | "y": 0, 17 | "z": 10 18 | } 19 | ], 20 | "lookat": [ 21 | { 22 | "x": 0, 23 | "y": 0, 24 | "z": 0 25 | }, 26 | { 27 | "x": 0, 28 | "y": 5, 29 | "z": 0 30 | }, 31 | { 32 | "x": 0, 33 | "y": 10, 34 | "z": -10 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/camera-transitions/data/dolly-data-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": 20, 3 | "origin": [ 4 | { 5 | "x": -35, 6 | "y": 0, 7 | "z": 0 8 | }, 9 | { 10 | "x": 0, 11 | "y": 10, 12 | "z": 20 13 | }, 14 | { 15 | "x": 40, 16 | "y": 0, 17 | "z": 0 18 | } 19 | ], 20 | "lookat": [ 21 | { 22 | "x": -40, 23 | "y": 10, 24 | "z": -40 25 | }, 26 | { 27 | "x": 0, 28 | "y": -10, 29 | "z": -40 30 | }, 31 | { 32 | "x": 40, 33 | "y": 10, 34 | "z": -40 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "WebGL React App", 3 | "name": "Create WebGL React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo180.png", 12 | "type": "image/png", 13 | "sizes": "180x180" 14 | }, 15 | { 16 | "src": "logo256.png", 17 | "type": "image/png", 18 | "sizes": "256x256" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/webgl-app/utils/stats.js: -------------------------------------------------------------------------------- 1 | import settings from '../settings'; 2 | import RendererStats from './render-stats'; 3 | 4 | const rendererStats = new RendererStats(); 5 | if (settings.stats) { 6 | rendererStats.domElement.style.position = 'absolute'; 7 | rendererStats.domElement.style.left = '0px'; 8 | rendererStats.domElement.style.top = '48px'; 9 | if (document.body) document.body.appendChild(rendererStats.domElement); 10 | } 11 | 12 | export { rendererStats }; 13 | 14 | if (settings.stats) { 15 | const stats = require('@jam3/stats')(); 16 | stats.domElement.style.cssText = 'position:fixed;left:0;top:0;z-index:10000'; 17 | } 18 | -------------------------------------------------------------------------------- /src/webgl-app/objects/background/shader.glsl.js: -------------------------------------------------------------------------------- 1 | export const vertexShader = ` 2 | varying vec2 vUv; 3 | void main() { 4 | vUv = uv; 5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 6 | } 7 | `; 8 | 9 | export const fragmentShader = ` 10 | uniform vec3 color0; 11 | uniform vec3 color1; 12 | uniform float strength; 13 | uniform float powStrength; 14 | varying vec2 vUv; 15 | 16 | void main() { 17 | float y = distance(vec2(0.5), vec2(0.5, vUv.y)) * strength; 18 | y = pow(y, powStrength); 19 | vec3 color = mix(color0, color1, y); 20 | gl_FragColor = vec4(color, 1.0); 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /src/webgl-app/loading/loaders/image-loader.js: -------------------------------------------------------------------------------- 1 | import Loader from './loader'; 2 | import Asset from '../asset'; 3 | 4 | /** 5 | * Image Loader 6 | * 7 | * @export 8 | * @class ImageLoader 9 | * @extends {Loader} 10 | */ 11 | export default class ImageLoader extends Loader { 12 | constructor(asset: Asset) { 13 | super(); 14 | this.asset = asset; 15 | } 16 | 17 | load = () => { 18 | const image = new Image(); 19 | 20 | image.onload = () => { 21 | this.asset.data = image; 22 | this.emit('loaded', this.asset); 23 | }; 24 | 25 | image.onerror = () => { 26 | this.emit('error', `Failed to load ${this.asset.src}`); 27 | }; 28 | 29 | image.src = this.asset.src; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/webgl-app/loading/loaders/three-fbx-loader.js: -------------------------------------------------------------------------------- 1 | import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'; 2 | import Loader from './loader'; 3 | import Asset from '../asset'; 4 | 5 | /** 6 | * Threejs FBX Loader 7 | * 8 | * @export 9 | * @class ThreeFBXLoader 10 | * @extends {Loader} 11 | */ 12 | export default class ThreeFBXLoader extends Loader { 13 | constructor(asset: Asset) { 14 | super(); 15 | this.asset = asset; 16 | } 17 | 18 | load = () => { 19 | const loader = new FBXLoader(); 20 | 21 | const onLoaded = (data: Object) => { 22 | this.asset.data = data; 23 | this.emit('loaded', this.asset); 24 | }; 25 | 26 | const onError = () => { 27 | this.emit('error', `Failed to load ${this.asset.src}`); 28 | }; 29 | 30 | loader.load(this.asset.src, onLoaded, null, onError); 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/webgl-app/loading/loaders/three-texture-loader.js: -------------------------------------------------------------------------------- 1 | import { TextureLoader, Texture } from 'three'; 2 | import Loader from './loader'; 3 | import Asset from '../asset'; 4 | 5 | /** 6 | * Threejs texture loader 7 | * 8 | * @export 9 | * @class ThreeTextureLoader 10 | * @extends {Loader} 11 | */ 12 | export default class ThreeTextureLoader extends Loader { 13 | constructor(asset: Asset) { 14 | super(); 15 | this.asset = asset; 16 | } 17 | 18 | load = () => { 19 | const loader = new TextureLoader(); 20 | 21 | const onLoaded = (texture: Texture) => { 22 | this.asset.data = texture; 23 | this.emit('loaded', this.asset); 24 | }; 25 | 26 | const onError = () => { 27 | this.emit('error', `Failed to load ${this.asset.src}`); 28 | }; 29 | 30 | loader.load(this.asset.src, onLoaded, null, onError); 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[Feature] TODO_CHANGE_FEATURE_TITLE ' 5 | labels: '' 6 | assignees: amelierosser 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **What does the proposed API look like?** 19 | What kind of methods it will have, how do you think you will use it? 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /src/webgl-app/loading/loaders/json-loader.js: -------------------------------------------------------------------------------- 1 | import Loader from './loader'; 2 | import Asset from '../asset'; 3 | 4 | /** 5 | * Json loader 6 | * 7 | * @export 8 | * @class JsonLoader 9 | * @extends {Loader} 10 | */ 11 | export default class JsonLoader extends Loader { 12 | constructor(asset: Asset) { 13 | super(); 14 | this.asset = asset; 15 | } 16 | 17 | load = () => { 18 | const req = new XMLHttpRequest(); 19 | 20 | req.onreadystatechange = () => { 21 | if (req.readyState !== 4) return; 22 | if (req.readyState === 4 && req.status === 200) { 23 | this.asset.data = JSON.parse(req.responseText); 24 | this.emit('loaded', this.asset); 25 | } else { 26 | this.emit('error', `Failed to load ${this.asset.src}`); 27 | } 28 | }; 29 | 30 | req.open('GET', this.asset.src, true); 31 | req.send(); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/webgl-app/utils/dispose-objects.js: -------------------------------------------------------------------------------- 1 | import { Object3D, Scene } from 'three'; 2 | 3 | /** 4 | * Recursively dispose threejs objects 5 | * 6 | * @export 7 | * @param {Object3D} object 8 | * @param {(Scene | Object3D)} parent 9 | * @returns 10 | */ 11 | export default function disposeObjects(object: Scene | Object3D, parent: Scene | Object3D) { 12 | if (object === null || object === undefined) return; 13 | if (parent) parent.remove(object); 14 | if (object.dispose) { 15 | object.dispose(); 16 | } 17 | if (object.geometry) { 18 | object.geometry.dispose(); 19 | } 20 | if (object.material) { 21 | object.material.dispose(); 22 | } 23 | if (object.children) { 24 | let i = 0; 25 | const l = object.children.length; 26 | while (i < l) { 27 | disposeObjects(object.children[0], object); 28 | i++; 29 | } 30 | } 31 | if (object.type === 'Scene') object.dispose(); 32 | object = null; 33 | } 34 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/landing/objects/jam3/shader.glsl.js: -------------------------------------------------------------------------------- 1 | export const vertexShader = ` 2 | varying vec3 vWorldPosition; 3 | varying vec3 vNormal; 4 | void main() { 5 | vNormal = normal; 6 | vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz; 7 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 8 | } 9 | `; 10 | 11 | export const fragmentShader = ` 12 | uniform float fresnelPow; 13 | uniform vec2 resolution; 14 | varying vec3 vNormal; 15 | varying vec3 vWorldPosition; 16 | uniform sampler2D particleMap; 17 | 18 | void main() { 19 | vec2 uv = gl_FragCoord.xy / resolution; 20 | vec3 texel = texture2D(particleMap, uv).rgb; 21 | vec3 normal = normalize( vNormal ); 22 | vec3 eye = cameraPosition - vWorldPosition.xyz; 23 | float cosTheta = abs(dot(normalize(eye), normal)); 24 | float fresnel = pow(cosTheta, fresnelPow); 25 | vec3 shine = vec3(fresnel); 26 | gl_FragColor = vec4(mix(texel, shine, 0.25), 1.0); 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /src/webgl-app/loading/asset-loader.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import Asset from './asset'; 3 | import GroupLoader from './loaders/group-loader'; 4 | 5 | /** 6 | * Assetloader configures an instance of GroupLoader 7 | * and should be used for loading any asset groups for the asset manager 8 | * 9 | * @class AssetLoader 10 | * @extends {EventEmitter} 11 | */ 12 | class AssetLoader extends EventEmitter { 13 | load = (id: string, assets: Asset[]) => { 14 | const loader = new GroupLoader({ id }); 15 | assets.forEach(asset => { 16 | if (asset.args === undefined) asset.args = {}; 17 | }); 18 | 19 | loader.on('progress', response => { 20 | this.emit('progress', response); 21 | }); 22 | 23 | loader.once('loaded', response => { 24 | this.emit('loaded', response); 25 | }); 26 | 27 | loader.once('error', error => { 28 | this.emit('error', error); 29 | }); 30 | 31 | loader.load(assets); 32 | }; 33 | } 34 | 35 | export default new AssetLoader(); 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[Bug] TODO_CHANGE_BUG_TITLE' 5 | labels: bug 6 | assignees: amelierosser 7 | --- 8 | 9 | ## Describe the bug 10 | 11 | 12 | 13 | ## To Reproduce 14 | 15 | 24 | 25 | ## Screenshots 26 | 27 | 28 | 29 | ## Expected behaviour 30 | 31 | 32 | 33 | ## Environment 34 | 35 | Run the below command and paste the result here: 36 | 37 | ``` 38 | npx envinfo --system --npmPackages react* --binaries --npmGlobalPackages react* --browsers 39 | ``` 40 | 41 | ## Additional context 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/webgl-app/settings.js: -------------------------------------------------------------------------------- 1 | import { getQueryFromParams } from './utils/query-params'; 2 | 3 | const settings = {}; 4 | 5 | // Enviroment setting 6 | settings.isDevelopment = process.env.NODE_ENV !== 'production'; 7 | 8 | // Base url 9 | settings.baseUrl = process.env.PUBLIC_URL || ''; 10 | 11 | // Show fps stats 12 | settings.stats = getQueryFromParams('stats') === null; 13 | 14 | // Enable dev camera rendering 15 | settings.devCamera = getQueryFromParams('devCamera') === 'true'; 16 | 17 | // Enable helpers 18 | settings.helpers = getQueryFromParams('helpers') === 'true'; 19 | 20 | // Enable dat gui 21 | settings.datGui = getQueryFromParams('gui') === null; 22 | 23 | // Skips all transitions 24 | settings.skipTransitions = getQueryFromParams('skipTransitions') === null; 25 | 26 | // GUI Number precision 27 | settings.guiPrecision = 0.001; 28 | 29 | // Viewport preview scale (when using devCamera) 30 | settings.viewportPreviewScale = 0.25; 31 | 32 | // Unlock full render size (should be false for prod) 33 | settings.renderBufferFullscreen = false; 34 | 35 | export default settings; 36 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/interactive-sphere/objects/sphere/shader.glsl.js: -------------------------------------------------------------------------------- 1 | import { simplexNoise3D } from '../../../../shaders/noise.glsl'; 2 | 3 | export default { 4 | uniforms: { 5 | time: { value: 0 } 6 | }, 7 | vertexShader: { 8 | uniforms: ` 9 | uniform float time; 10 | varying vec3 vNormal; 11 | `, 12 | functions: ` 13 | ${simplexNoise3D} 14 | `, 15 | preTransform: ``, 16 | postTransform: ` 17 | float speed = time * 0.5; 18 | float noise = simplexNoise3D(position.xyz * 0.75 + speed) * 0.15; 19 | transformed = normal * noise; 20 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position + transformed, 1.0); 21 | vNormal = normal; 22 | ` 23 | }, 24 | fragmentShader: { 25 | uniforms: ` 26 | varying vec3 vNormal; 27 | `, 28 | functions: ``, 29 | preFragColor: ` 30 | vec3 normal = normalize(vNormal); 31 | normal.b = 1.0; 32 | outgoingLight *= (normal * 0.5 + 0.5); 33 | `, 34 | postFragColor: ` 35 | gl_FragColor.a = opacity; 36 | ` 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jam3 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/webgl-app/utils/query-params.js: -------------------------------------------------------------------------------- 1 | const queryString = require('query-string'); 2 | 3 | /** 4 | * Get a query parameter 5 | * 6 | * @export 7 | * @param {String} prop 8 | * @returns 9 | */ 10 | export function getQueryFromParams(prop: string): mixed { 11 | const params = queryString.parse(window.location.search); 12 | return params[prop] !== undefined ? params[prop] : false; 13 | } 14 | 15 | /** 16 | * Set a query parmeter 17 | * 18 | * @export 19 | * @param {String} query 20 | * @param {String} val 21 | * @param {boolean} [reload=false] 22 | * @returns 23 | */ 24 | export function setQuery(query: string, val: string, reload: boolean = false) { 25 | const queries = queryString.parse(window.location.search); 26 | const newQueries = Object.assign({}, queries, { 27 | [query]: val 28 | }); 29 | const stringified = queryString.stringify(newQueries); 30 | 31 | if (reload) { 32 | window.location.href = `${window.location.pathname}?${stringified}`; 33 | return; 34 | } 35 | const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?${stringified}`; 36 | window.history.pushState({ path: url }, '', url); 37 | } 38 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/post-processing/passes/final-pass/shader.glsl.js: -------------------------------------------------------------------------------- 1 | import { fragmentUniforms as fxaaFragmentUniforms, 2 | fragmentMain as fxaaFragmentMain, 3 | fragmentPass as fxaaFragmentPass } 4 | from '../../passes/fxaa.glsl'; 5 | 6 | import { fragmentUniforms as filmFragmentUniforms, 7 | fragmentMain as filmFragmentMain, 8 | fragmentPass as filmFragmentPass } 9 | from '../../passes/film.glsl'; 10 | 11 | export const vertexShader = ` 12 | void main() { 13 | gl_Position = vec4(position, 1.0); 14 | } 15 | `; 16 | 17 | export const fragmentShader = ` 18 | uniform vec2 resolution; 19 | uniform float time; 20 | uniform sampler2D tDiffuse; 21 | // FXAA pass 22 | ${fxaaFragmentUniforms} 23 | ${fxaaFragmentPass} 24 | // Film pass 25 | ${filmFragmentUniforms} 26 | ${filmFragmentPass} 27 | void main() { 28 | vec2 uv = gl_FragCoord.xy / resolution; 29 | vec4 outgoingColor = texture2D(tDiffuse, uv); 30 | // FXAA pass 31 | ${fxaaFragmentMain} 32 | // Film pass 33 | ${filmFragmentMain} 34 | gl_FragColor.rgb = outgoingColor.rgb; 35 | gl_FragColor.a = outgoingColor.a; 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /src/webgl-app/lights/ambient.js: -------------------------------------------------------------------------------- 1 | import { AmbientLight } from 'three'; 2 | import { GUI } from 'dat.gui'; 3 | import settings from '../settings'; 4 | 5 | /** 6 | * Utility for creating ambient lights 7 | * 8 | * @export 9 | * @class Ambient 10 | */ 11 | export default class Ambient { 12 | settings: Object; 13 | light: AmbientLight; 14 | gui: GUI; 15 | guiParent: GUI; 16 | 17 | constructor(options: Object = {}) { 18 | this.settings = Object.assign( 19 | { 20 | color: 0xd4d4d4, 21 | intensity: 0.6, 22 | guiOpen: false 23 | }, 24 | options 25 | ); 26 | this.light = new AmbientLight(this.settings.color, this.settings.intensity); 27 | } 28 | 29 | gui(guiParent: GUI) { 30 | this.guiParent = guiParent; 31 | this.gui = guiParent.addFolder('ambient'); 32 | if (this.settings.guiOpen) this.gui.open(); 33 | this.gui.add(this.light, 'intensity', 0, 1, settings.guiPrecision); 34 | this.gui.addColor(this.settings, 'color').onChange(this.onChange); 35 | } 36 | 37 | onChange = () => { 38 | this.light.color.setHex(this.settings.color); 39 | }; 40 | 41 | dispose() { 42 | this.guiParent.removeFolder(this.gui.name); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/webgl-app/loading/loaders/three-gltf-loader.js: -------------------------------------------------------------------------------- 1 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; 2 | import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; 3 | import Loader from './loader'; 4 | import Asset from '../asset'; 5 | import settings from '../../settings'; 6 | 7 | // Use the draco loader for gltf if the glb file is compressed with draco 8 | const dracoLoader = new DRACOLoader(); 9 | dracoLoader.setDecoderPath(`${settings.baseUrl}/assets/lib/draco/gltf/`); 10 | dracoLoader.preload(); 11 | 12 | /** 13 | * Threejs GLTF Loader 14 | * 15 | * @export 16 | * @class ThreeGLTFLoader 17 | * @extends {Loader} 18 | */ 19 | export default class ThreeGLTFLoader extends Loader { 20 | constructor(asset: Asset) { 21 | super(); 22 | this.asset = asset; 23 | } 24 | 25 | load = () => { 26 | const loader = new GLTFLoader(); 27 | loader.setDRACOLoader(dracoLoader); 28 | 29 | const onLoaded = (gltf: Object) => { 30 | this.asset.data = gltf; 31 | this.emit('loaded', this.asset); 32 | }; 33 | 34 | const onError = () => { 35 | this.emit('error', `Failed to load ${this.asset.src}`); 36 | }; 37 | 38 | loader.load(this.asset.src, onLoaded, null, onError); 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/renderer.js: -------------------------------------------------------------------------------- 1 | import { WebGLRenderer } from 'three'; 2 | import graphics, { getGraphicsMode, getTier } from './graphics'; 3 | import settings from '../settings'; 4 | import { setRendererSize } from './resize'; 5 | import PostProcessing from './post-processing/post-processing'; 6 | import { gui } from '../utils/gui'; 7 | 8 | const { pixelRatio, antialias } = graphics[getGraphicsMode()]; 9 | 10 | const renderer = new WebGLRenderer({ 11 | antialias, 12 | powerPreference: 'high-performance', 13 | stencil: false 14 | }); 15 | renderer.setClearColor(0x000000); 16 | 17 | // Enable shader errors during dev 18 | renderer.debug.checkShaderErrors = settings.isDevelopment; 19 | 20 | const guiRendering = gui.addFolder('rendering'); 21 | guiRendering.open(); 22 | 23 | renderer.setPixelRatio(pixelRatio); 24 | renderer.setScissorTest(true); 25 | setRendererSize(renderer, window.innerWidth, window.innerHeight); 26 | 27 | export const postProcessing = new PostProcessing(guiRendering); 28 | 29 | const gl = renderer.getContext(); 30 | const gpuInfo = gl.getExtension('WEBGL_debug_renderer_info'); 31 | const gpu = gl.getParameter(gpuInfo.UNMASKED_RENDERER_WEBGL); 32 | 33 | if (settings.isDevelopment) console.log(`Graphics: ${getGraphicsMode()}\nGPU: ${gpu}\nTier: ${getTier()}`); 34 | 35 | export default renderer; 36 | -------------------------------------------------------------------------------- /src/webgl-app/cameras/cameras.js: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera, Vector3 } from 'three'; 2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 3 | import renderer from '../rendering/renderer'; 4 | import { VECTOR_ZERO, VECTOR_ONE } from '../utils/math'; 5 | 6 | // Perspective camera defaults 7 | export const FOV = 65; 8 | const near = 0.1; 9 | const far = 1000; 10 | 11 | /** 12 | * Reset the camera position 13 | * 14 | * @export 15 | * @param {PerspectiveCamera} camera 16 | * @param {number} [zoom=1] 17 | * @param {Vector3} [angle=VECTOR_ONE] 18 | */ 19 | export function resetCamera(camera: PerspectiveCamera, zoom: number = 1, angle: Vector3 = VECTOR_ONE) { 20 | camera.position.set(angle.x * zoom, angle.y * zoom, angle.z * zoom); 21 | camera.lookAt(VECTOR_ZERO); 22 | } 23 | 24 | /** 25 | * Utility for creating a perspective camera 26 | * 27 | * @export 28 | * @returns 29 | */ 30 | export function createPerspectiveCamera(aspect: number): PerspectiveCamera { 31 | return new PerspectiveCamera(FOV, aspect, near, far); 32 | } 33 | 34 | /** 35 | * Utility for creating orbit controls 36 | * 37 | * @export 38 | * @param {PerspectiveCamera} camera 39 | * @returns 40 | */ 41 | export function createOrbitControls(camera: PerspectiveCamera): OrbitControls { 42 | return new OrbitControls(camera, renderer.domElement); 43 | } 44 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | **What kind of change does this PR introduce?** (check at least one) 8 | 9 | - [ ] Bugfix (non-breaking change which fixes an issue) 10 | - [ ] Feature (non-breaking change which adds functionality) 11 | - [ ] Code style update 12 | - [ ] Refactor (refactoring or adding test which isn't a fix or add a feature) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] Build-related changes 15 | - [ ] Other, please describe: 16 | 17 | **Does this PR introduce a breaking change?** (check one) 18 | 19 | - [ ] Yes 20 | - [ ] No 21 | 22 | **Did you test your solution?** 23 | 24 | - [ ] I lightly tested it in one browser 25 | - [ ] I deeply tested it in several browsers 26 | - [ ] I wrote tests around it (unit tests, integration tests, E2E tests) 27 | 28 | ## Problem Description 29 | 30 | 31 | 32 | ## Solution Description 33 | 34 | 35 | 36 | ## Side Effects, Risks, Impact 37 | 38 | 39 | 40 | - [ ] N/A 41 | 42 | **Aditional comments:** 43 | -------------------------------------------------------------------------------- /src/webgl-app/loading/asset-manager.js: -------------------------------------------------------------------------------- 1 | import Asset from './asset'; 2 | 3 | /** 4 | * Asset manager's purpose is to store loaded assets by the AssetLoader 5 | * Assets can be retrived by using the get() function 6 | * 7 | * @class AssetManager 8 | */ 9 | class AssetManager { 10 | assets: Object; 11 | 12 | constructor() { 13 | this.assets = {}; 14 | } 15 | 16 | /** 17 | * Add an asset group 18 | * 19 | * @param {String} group 20 | * @param {Asset[]} assets 21 | * @memberof AssetManager 22 | */ 23 | add(group: String, assets: Asset[]) { 24 | this.assets[group] = this.assets[group] || []; 25 | this.assets[group].push(...assets); 26 | } 27 | 28 | /** 29 | * Retrieve an asset by id 30 | * 31 | * @param {String} groupId 32 | * @param {String} id 33 | * @param {Boolean} [all=false] 34 | * @returns 35 | * @memberof AssetManager 36 | */ 37 | get(groupId: string, id: string, all: boolean = false): boolean | mixed { 38 | // console.log('groupId', groupId, 'id', id); 39 | const asset = this.find(this.assets[groupId], id); 40 | if (asset && asset instanceof Asset) { 41 | return all ? asset : asset.data; 42 | } 43 | return false; 44 | } 45 | 46 | /** 47 | * Find an asset by id 48 | * 49 | * @param {Asset[]} assets 50 | * @param {String} id 51 | * @returns 52 | * @memberof AssetManager 53 | */ 54 | find(assets: Asset[], id: string): boolean | Asset { 55 | return assets.find(asset => asset.id === id) || false; 56 | } 57 | } 58 | 59 | export default new AssetManager(); 60 | -------------------------------------------------------------------------------- /scripts/assets/optimise.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const shell = require('shelljs'); 3 | const fileName = require('file-name'); 4 | const fileExtension = require('file-extension'); 5 | const ModelOptimiser = require('./model-optimiser'); 6 | const TextureOptimiser = require('./texture-optimiser'); 7 | 8 | const ASSETS_SRC = './src/assets'; 9 | const DESTINATION_DEST = './public/assets/webgl'; 10 | 11 | /* 12 | * Loop through all files locacted in the source directory 13 | * and run optimisers on any directories found 14 | */ 15 | shell.ls(ASSETS_SRC).forEach(value => { 16 | const directory = `${ASSETS_SRC}/${value}`; 17 | 18 | if (!fs.lstatSync(directory).isDirectory()) { 19 | shell.echo(`Assets needs to be placed in a directory: ${value}`); 20 | return; 21 | } 22 | 23 | const textureOptimiser = new TextureOptimiser(); 24 | const modelOptimiser = new ModelOptimiser(); 25 | 26 | // Loop through files of the directory 27 | shell.ls('-A', directory).forEach(file => { 28 | const filename = fileName(file); 29 | const extension = fileExtension(file); 30 | if (textureOptimiser.includes(file)) textureOptimiser.add(directory, filename, extension); 31 | if (modelOptimiser.includes(file)) modelOptimiser.add(directory, filename, extension); 32 | }); 33 | 34 | // Process all optimisers 35 | Promise.all([ 36 | textureOptimiser.process(value, directory, DESTINATION_DEST), 37 | modelOptimiser.process(value, directory, DESTINATION_DEST) 38 | ]) 39 | .then(() => { 40 | console.log('Processed assets!'); 41 | }) 42 | .catch(error => { 43 | console.log('Error processing assets', error); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/preload-gpu.js: -------------------------------------------------------------------------------- 1 | import { Scene, PerspectiveCamera, RGBAFormat, Object3D } from 'three'; 2 | import renderer from './renderer'; 3 | import { createRenderTarget } from './render-target'; 4 | import RenderTargetHelper from '../utils/render-target-helper'; 5 | 6 | const RENDER_TARGET_SIZE = 128; 7 | const RENDER_TARGET_DEBUG = false; 8 | 9 | const renderTarget = createRenderTarget(RENDER_TARGET_SIZE, RENDER_TARGET_SIZE, { 10 | depthBuffer: false, 11 | format: RGBAFormat 12 | }); 13 | 14 | let renderTargetHelper; 15 | if (RENDER_TARGET_DEBUG) { 16 | renderTargetHelper = new RenderTargetHelper(renderTarget); 17 | } 18 | 19 | // https://medium.com/@hellomondaycom/how-we-built-the-google-cloud-infrastructure-webgl-experience-dec3ce7cd209 20 | function setAllCulled(obj: Object3D, overrideCulled: boolean) { 21 | if (overrideCulled === false) { 22 | obj.wasFrustumCulled = obj.frustumCulled; 23 | obj.wasVisible = obj.visible; 24 | obj.visible = true; 25 | obj.frustumCulled = false; 26 | } else { 27 | obj.visible = obj.wasVisible; 28 | obj.frustumCulled = obj.wasFrustumCulled; 29 | } 30 | obj.children.forEach(child => setAllCulled(child, overrideCulled)); 31 | } 32 | 33 | export default function preloadGpu(scene: Scene, camera: PerspectiveCamera) { 34 | const cameraAspect = camera.aspect; 35 | camera.aspect = 1; 36 | camera.updateProjectionMatrix(); 37 | setAllCulled(scene, false); 38 | renderer.setRenderTarget(renderTarget); 39 | renderer.render(scene, camera); 40 | if (RENDER_TARGET_DEBUG) renderTargetHelper.update(); 41 | renderer.setRenderTarget(null); 42 | camera.aspect = cameraAspect; 43 | camera.updateProjectionMatrix(); 44 | setAllCulled(scene, true); 45 | } 46 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/resize.js: -------------------------------------------------------------------------------- 1 | import { Vector2, WebGLRenderer } from 'three'; 2 | import graphics, { getGraphicsMode } from './graphics'; 3 | import settings from '../settings'; 4 | 5 | const { maxFrameBufferSize, pixelRatio } = graphics[getGraphicsMode()]; 6 | 7 | const baseSize = Math.sqrt(maxFrameBufferSize.x * maxFrameBufferSize.y); 8 | const maxSize = baseSize * baseSize; 9 | 10 | export const rendererSize = new Vector2(); 11 | 12 | export function getRenderBufferSize(): { width: number, height: number } { 13 | return { 14 | width: rendererSize.x * pixelRatio, 15 | height: rendererSize.y * pixelRatio 16 | }; 17 | } 18 | 19 | function resize(windowWidth: number, windowHeight: number): { width: number, height: number } { 20 | let width = windowWidth; 21 | let height = windowHeight; 22 | if (windowWidth * windowHeight > maxSize) { 23 | const ratio = height / width; 24 | width = baseSize; 25 | height = Math.floor(baseSize * ratio); 26 | let newSize = width * height; 27 | const scalar = Math.sqrt(maxSize / newSize); 28 | width = Math.floor(width * scalar); 29 | height = Math.floor(height * scalar); 30 | } 31 | return { 32 | width, 33 | height 34 | }; 35 | } 36 | 37 | export function setRendererSize(renderer: WebGLRenderer, windowWidth: number, windowHeight: number) { 38 | let { width, height } = resize(windowWidth, windowHeight); 39 | if (settings.renderBufferFullscreen) { 40 | width = windowWidth; 41 | height = windowHeight; 42 | } 43 | rendererSize.x = width; 44 | rendererSize.y = height; 45 | renderer.setSize(width, height); 46 | renderer.domElement.style.width = `${windowWidth}px`; 47 | renderer.domElement.style.height = `${windowHeight}px`; 48 | } 49 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/graphics.js: -------------------------------------------------------------------------------- 1 | import { getGPUTier } from 'detect-gpu'; 2 | import { Vector2 } from 'three'; 3 | import math from '../utils/math'; 4 | import { getQueryFromParams } from '../utils/query-params'; 5 | 6 | // Graphics mode constants 7 | export const GRAPHICS_HIGH = 'high'; 8 | export const GRAPHICS_NORMAL = 'normal'; 9 | export const GRAPHICS_MODES = [GRAPHICS_HIGH, GRAPHICS_NORMAL]; 10 | 11 | let GRAPHICS_MODE = GRAPHICS_NORMAL; 12 | 13 | /** 14 | * Get the current graphics mode 15 | * 16 | * @export 17 | * @returns 18 | */ 19 | export function getGraphicsMode(): string { 20 | return GRAPHICS_MODE; 21 | } 22 | 23 | const gpuTier = getGPUTier(); 24 | 25 | export function profiler(): string { 26 | switch (gpuTier.tier) { 27 | case 'GPU_DESKTOP_TIER_3': 28 | case 'GPU_DESKTOP_TIER_2': 29 | case 'GPU_MOBILE_TIER_3': 30 | return GRAPHICS_HIGH; 31 | case 'GPU_DESKTOP_TIER_1': 32 | default: 33 | return GRAPHICS_NORMAL; 34 | } 35 | } 36 | 37 | // If the graphics query parameter is set, use it over the current gpu tier 38 | const graphicsMode = getQueryFromParams('graphics'); 39 | if (GRAPHICS_MODES.includes(graphicsMode) && typeof graphicsMode === 'string') { 40 | GRAPHICS_MODE = graphicsMode; 41 | } else { 42 | GRAPHICS_MODE = profiler(); 43 | } 44 | 45 | export function getTier(): string { 46 | return gpuTier.tier; 47 | } 48 | 49 | // Graphics settings for the renderer 50 | export default { 51 | [GRAPHICS_HIGH]: { 52 | antialias: false, // Enable antialias if you're not using post processing 53 | pixelRatio: math.clamp(window.devicePixelRatio, 1, 2), 54 | maxFrameBufferSize: new Vector2(1280, 720) 55 | }, 56 | [GRAPHICS_NORMAL]: { 57 | antialias: false, 58 | pixelRatio: 1, 59 | maxFrameBufferSize: new Vector2(1280, 720) 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | WebGL React App 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/webgl-app/lights/directional.js: -------------------------------------------------------------------------------- 1 | import { DirectionalLight, DirectionalLightHelper } from 'three'; 2 | import { GUI } from 'dat.gui'; 3 | import settings from '../settings'; 4 | 5 | /** 6 | * Utility for creating directional lights 7 | * 8 | * @export 9 | * @class Directional 10 | */ 11 | export default class Directional { 12 | settings: Object; 13 | light: DirectionalLight; 14 | gui: GUI; 15 | guiParent: GUI; 16 | helper: DirectionalLightHelper; 17 | 18 | constructor(options: Object = {}) { 19 | this.settings = Object.assign( 20 | { 21 | color: 0xd4d4d4, 22 | intensity: 0.6, 23 | guiOpen: false 24 | }, 25 | options 26 | ); 27 | this.light = new DirectionalLight(this.settings.color, this.settings.intensity); 28 | this.light.position.set(1, 1, 1); 29 | this.helper = new DirectionalLightHelper(this.light); 30 | } 31 | 32 | gui(guiParent: GUI) { 33 | this.guiParent = guiParent; 34 | this.gui = guiParent.addFolder('directional'); 35 | if (this.settings.guiOpen) this.gui.open(); 36 | this.gui.addColor(this.settings, 'color').onChange(this.onChange); 37 | this.gui.add(this.light, 'intensity', 0, 1, settings.guiPrecision); 38 | const range = 1; 39 | this.gui 40 | .add(this.light.position, 'x', -range, range) 41 | .step(settings.guiPrecision) 42 | .name('direction x'); 43 | this.gui 44 | .add(this.light.position, 'y', -range, range) 45 | .step(settings.guiPrecision) 46 | .name('direction y'); 47 | this.gui 48 | .add(this.light.position, 'z', -range, range) 49 | .step(settings.guiPrecision) 50 | .name('direction z'); 51 | } 52 | 53 | onChange = () => { 54 | this.light.color.setHex(this.settings.color); 55 | this.helper.update(); 56 | }; 57 | 58 | dispose() { 59 | this.guiParent.removeFolder(this.gui.name); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/webgl-app/lights/point.js: -------------------------------------------------------------------------------- 1 | import { PointLight } from 'three'; 2 | import { GUI } from 'dat.gui'; 3 | import settings from '../settings'; 4 | 5 | /** 6 | * Utility for creating point lights 7 | * 8 | * @export 9 | * @class Point 10 | */ 11 | export class Point { 12 | settings: Object; 13 | light: PointLight; 14 | gui: GUI; 15 | guiParent: GUI; 16 | 17 | constructor(options: Object = {}) { 18 | this.settings = Object.assign( 19 | { 20 | color: 0xd4d4d4, 21 | intensity: 0.6, 22 | distance: 100, 23 | decay: 0, 24 | guiOpen: false 25 | }, 26 | options 27 | ); 28 | this.light = new PointLight( 29 | this.settings.color, 30 | this.settings.intensity, 31 | this.settings.distance, 32 | this.settings.decay 33 | ); 34 | this.light.position.set(1, 1, 1); 35 | } 36 | 37 | gui(guiParent: GUI) { 38 | this.guiParent = guiParent; 39 | this.gui = guiParent.addFolder('point'); 40 | if (this.settings.guiOpen) this.gui.open(); 41 | const range = 100; 42 | this.gui.addColor(this.settings, 'color').onChange(this.onChange); 43 | this.gui.add(this.settings, 'intensity', 0, 10, settings.guiPrecision); 44 | this.gui.add(this.settings, 'distance', 0, 1000); 45 | this.gui.add(this.settings, 'decay', 0, 1000); 46 | this.gui 47 | .add(this.light.position, 'x', -range, range) 48 | .step(settings.guiPrecision) 49 | .name('position x'); 50 | this.gui 51 | .add(this.light.position, 'y', -range, range) 52 | .step(settings.guiPrecision) 53 | .name('position y'); 54 | this.gui 55 | .add(this.light.position, 'z', -range, range) 56 | .step(settings.guiPrecision) 57 | .name('position z'); 58 | } 59 | 60 | onChange = () => { 61 | this.light.color.setHex(this.settings.color); 62 | }; 63 | 64 | dispose() { 65 | this.guiParent.removeFolder(this.gui.name); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/interactive-sphere/interactive-sphere-scene.js: -------------------------------------------------------------------------------- 1 | import BaseScene from '../base/base-scene'; 2 | import { VECTOR_ZERO } from '../../utils/math'; 3 | import Sphere from './objects/sphere/sphere'; 4 | import Ambient from '../../lights/ambient'; 5 | import Directional from '../../lights/directional'; 6 | import assets from './assets'; 7 | import Background from '../../objects/background/background'; 8 | import settings from '../../settings'; 9 | 10 | export const INTERACTIVE_SPHERE_SCENE_ID = 'interactive-sphere'; 11 | 12 | export default class InteractiveSphereScene extends BaseScene { 13 | constructor() { 14 | settings.devCamera = false; 15 | const lights = [new Ambient(), new Directional()]; 16 | super({ id: INTERACTIVE_SPHERE_SCENE_ID, assets, gui: true, guiOpen: true, lights, controls: true }); 17 | this.camera.position.set(0, 0, 5); 18 | this.camera.lookAt(VECTOR_ZERO); 19 | } 20 | 21 | /** 22 | * Create and setup any objects for the scene 23 | * 24 | * @memberof LandingScene 25 | */ 26 | async createSceneObjects() { 27 | await new Promise((resolve, reject) => { 28 | try { 29 | this.background = new Background(this.gui); 30 | this.scene.add(this.background.mesh); 31 | this.sphere = new Sphere(this.camera); 32 | this.scene.add(this.sphere.mesh); 33 | this.animateInit(); 34 | resolve(); 35 | } catch (error) { 36 | reject(error); 37 | } 38 | }); 39 | } 40 | 41 | preloadGpuCullScene = (culled: boolean) => { 42 | this.sphere.preloadGpuCullScene(culled); 43 | }; 44 | 45 | animateInit = () => { 46 | return this.sphere.animateInit(); 47 | }; 48 | 49 | animateIn = () => { 50 | return this.sphere.animateIn(); 51 | }; 52 | 53 | animateOut = () => { 54 | return this.sphere.animateOut(); 55 | }; 56 | 57 | /** 58 | * Update loop 59 | * 60 | * @memberof LandingScene 61 | */ 62 | update = (delta: number) => { 63 | this.sphere.update(delta); 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/landing/objects/particles/shader.glsl.js: -------------------------------------------------------------------------------- 1 | export const vertexShader = ` 2 | attribute float size; // Per particle size attribute 3 | uniform float particleSize; // Uniform particle size (affects all) 4 | varying vec3 vPosition; // Vertex position for fragment shader 5 | 6 | void main() { 7 | // Model view position 8 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); 9 | // Scale point size based on distance 10 | gl_PointSize = size * (particleSize / length(mvPosition.xyz)); 11 | // Screen space projection 12 | gl_Position = projectionMatrix * mvPosition; 13 | // Set position varying 14 | vPosition = position; 15 | } 16 | `; 17 | 18 | export const fragmentShader = ` 19 | uniform vec3 lightDirection; 20 | uniform sampler2D normalMap; 21 | varying vec3 vPosition; 22 | 23 | // Signed distance function for 2D circle 24 | float circle(vec2 uv, vec2 pos, float rad) { 25 | float d = length(pos - uv) - rad; 26 | return step(d, 0.0); 27 | } 28 | 29 | void main() { 30 | float c = circle(vec2(0.5), gl_PointCoord.xy, 0.5); 31 | // Discard any pixels outside of circle 32 | if (c == 0.0) discard; 33 | 34 | vec4 outgoingColor = vec4(1.0); 35 | 36 | // Sample normal from texture, y coords are inverted from render target 37 | vec3 normal = texture2D(normalMap, vec2(gl_PointCoord.x, 1.0 - gl_PointCoord.y)).rgb * 2.0 - 1.0; 38 | // Second normal based on spherical particle position 39 | vec3 normal2 = normalize(vPosition); 40 | 41 | // Half lambert lighting model 42 | float intensity = max(0.2, dot(normal, lightDirection) * 0.5 + 0.5); 43 | float intensty2 = max(0.5, dot(normal2, lightDirection) * 0.5 + 0.5); 44 | 45 | // Base color on normal 46 | normal2.b = 1.0; 47 | vec3 color = vec3(normal2 * 0.5 + 0.5); 48 | 49 | // Apply lambert intensity 50 | color *= intensity; 51 | color *= intensty2; 52 | 53 | // Set outgoing color 54 | outgoingColor.rgb = color; 55 | 56 | gl_FragColor = outgoingColor; 57 | } 58 | `; 59 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "react-jam3"], 3 | "plugins": ["jam3"], 4 | "settings": { 5 | "react": { 6 | "version": "detect" 7 | } 8 | }, 9 | "rules": { 10 | "prettier/prettier": "warn", 11 | "jam3/no-sanitizer-with-danger": [ 12 | 2, 13 | { 14 | "wrapperName": ["sanitizer"] 15 | } 16 | ], 17 | "jam3/forbid-methods": 2, 18 | "no-console": "off", 19 | "react/sort-comp": [ 20 | 1, 21 | { 22 | "order": ["static-methods", "lifecycle", "everything-else", "render"], 23 | "groups": { 24 | "lifecycle": [ 25 | "displayName", 26 | "propTypes", 27 | "contextTypes", 28 | "childContextTypes", 29 | "mixins", 30 | "statics", 31 | "defaultProps", 32 | "constructor", 33 | "getDefaultProps", 34 | "getInitialState", 35 | "state", 36 | "getChildContext", 37 | "componentWillMount", 38 | "UNSAFE_componentWillMount", 39 | "componentDidMount", 40 | "componentWillReceiveProps", 41 | "UNSAFE_componentWillReceiveProps", 42 | "shouldComponentUpdate", 43 | "componentWillUpdate", 44 | "UNSAFE_componentWillUpdate", 45 | "componentDidUpdate", 46 | "componentWillUnmount", 47 | "componentWillAppear", 48 | "componentWillEnter", 49 | "componentWillLeave" 50 | ] 51 | } 52 | } 53 | ] 54 | }, 55 | "overrides": [ 56 | { 57 | "files": ["src/util/**/*.js"], 58 | "rules": { 59 | "require-jsdoc": [ 60 | "error", 61 | { 62 | "require": { 63 | "FunctionDeclaration": true, 64 | "ClassDeclaration": true, 65 | "MethodDefinition": false, 66 | "ArrowFunctionExpression": false, 67 | "FunctionExpression": false 68 | } 69 | } 70 | ] 71 | } 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this library 2 | 3 | Are you thinking about getting involved with the library? Great!, there are a few things you need to know. 4 | 5 | ## Code of Conduct 6 | 7 | We have adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. 8 | 9 | ## Branching 10 | 11 | Our `master` branch has the latest code, with tests passing after every PR is merged. However, if you want to use the latest stable version is recommended to download the latest release. 12 | 13 | ### Branching name convention 14 | 15 | `[topic]/[natural-name]` 16 | 17 | Where [topic] are the same types requires by [commitlint](.commitlintrc.yml) 18 | 19 | ## Semantic Versioning 20 | 21 | We follow [semantic versioning](http://semver.org/). We release patch versions for bugfixes, minor versions for new features, and major versions for any breaking changes. When we make breaking changes, we also introduce deprecation warnings in a minor version so that our users learn about the upcoming changes and migrate their code in advance. 22 | 23 | We tag every pull request with a label marking whether the change should go in the next patch, minor, or a major version. 24 | 25 | Every significant change is documented in the [changelog file](CHANGELOG.md). 26 | 27 | ## Bugs 28 | 29 | We are using GitHub Issues for our public bugs. Before creating a new issue, try to make sure your problem doesn’t already exist. 30 | 31 | We are using a template for our issues, using this template will reduce the time of understanding the issue and creating a solution. 32 | 33 | ## Pull Requests 34 | 35 | Pull Requests are welcome, please follow our template. 36 | 37 | You should always create Pull Request and never merge directly to `master`, and the `master` branch will be protected by default. 38 | 39 | ## Coding Style Guides 40 | 41 | To ensure following the same code style guidelines we are using in the repository Prettier and ESLint. By default, the repository is set up to avoid pushing if the coding rules are not being followed. 42 | -------------------------------------------------------------------------------- /src/webgl-app/objects/background/background.js: -------------------------------------------------------------------------------- 1 | import { Mesh, SphereBufferGeometry, BackSide, ShaderMaterial, Color } from 'three'; 2 | import { GUI } from 'dat.gui'; 3 | import { vertexShader, fragmentShader } from './shader.glsl'; 4 | import { GRAPHICS_HIGH, GRAPHICS_NORMAL, getGraphicsMode } from '../../rendering/graphics'; 5 | 6 | export default class Background { 7 | gui: GUI; 8 | guiParent: GUI; 9 | config: Object; 10 | mesh: Mesh; 11 | 12 | constructor(gui: GUI, radius: number = 50) { 13 | this.guiParent = gui; 14 | 15 | this.config = { 16 | color0: 0x000000, 17 | color1: 0x1b1b1b 18 | }; 19 | 20 | const material = new ShaderMaterial({ 21 | uniforms: { 22 | color0: { 23 | value: new Color(this.config.color0) 24 | }, 25 | color1: { 26 | value: new Color(this.config.color1) 27 | }, 28 | strength: { 29 | value: 2.5 30 | }, 31 | powStrength: { 32 | value: 1.3 33 | } 34 | }, 35 | vertexShader, 36 | fragmentShader, 37 | side: BackSide 38 | }); 39 | 40 | this.gui = gui.addFolder('background'); 41 | this.gui.open(); 42 | 43 | this.gui 44 | .add(material.uniforms.strength, 'value', 0, 10) 45 | .name('strength') 46 | .onChange(this.onChange); 47 | this.gui 48 | .add(material.uniforms.powStrength, 'value', 0, 10) 49 | .name('powStrength') 50 | .onChange(this.onChange); 51 | this.gui.addColor(this.config, 'color0').onChange(this.onChange); 52 | this.gui.addColor(this.config, 'color1').onChange(this.onChange); 53 | 54 | const divisionSettings = { 55 | [GRAPHICS_HIGH]: [32, 16], 56 | [GRAPHICS_NORMAL]: [18, 8] 57 | }; 58 | const divisions = divisionSettings[getGraphicsMode()]; 59 | 60 | this.mesh = new Mesh(new SphereBufferGeometry(radius, divisions[0], divisions[1]), material); 61 | this.mesh.matrixAutoUpdate = false; 62 | this.mesh.updateMatrix(); 63 | } 64 | 65 | onChange = () => { 66 | this.mesh.material.uniforms.color0.value.setHex(this.config.color0); 67 | this.mesh.material.uniforms.color1.value.setHex(this.config.color1); 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/webgl-app/utils/gui.js: -------------------------------------------------------------------------------- 1 | import detect from '@jam3/detect'; 2 | import settings from '../settings'; 3 | 4 | /** 5 | * @class Folder 6 | */ 7 | class Folder { 8 | add(object: any, key: string, list?: mixed[]) { 9 | return this; 10 | } 11 | listen() { 12 | return this; 13 | } 14 | name() { 15 | return this; 16 | } 17 | open() { 18 | return this; 19 | } 20 | close() { 21 | return this; 22 | } 23 | onChange(value: mixed) { 24 | return this; 25 | } 26 | addFolder(id: string) { 27 | return this; 28 | } 29 | addColor() { 30 | return this; 31 | } 32 | removeFolder(id: string) { 33 | return this; 34 | } 35 | remove() { 36 | return this; 37 | } 38 | step() { 39 | return this; 40 | } 41 | } 42 | 43 | /** 44 | * @class GUIWrapper 45 | */ 46 | class GUIWrapper { 47 | static toggleHide() { 48 | return this; 49 | } 50 | add(object: any, key: string, list?: mixed[]) { 51 | return this; 52 | } 53 | addFolder(id: string) { 54 | return new Folder(); 55 | } 56 | removeFolder(id: string) { 57 | return this; 58 | } 59 | addColor() { 60 | return this; 61 | } 62 | listen() { 63 | return this; 64 | } 65 | name() { 66 | return this; 67 | } 68 | close() { 69 | return this; 70 | } 71 | step() { 72 | return this; 73 | } 74 | onChange(value: mixed) { 75 | return this; 76 | } 77 | setValue() { 78 | return this; 79 | } 80 | remove() { 81 | return this; 82 | } 83 | open() { 84 | return this; 85 | } 86 | } 87 | 88 | let Cls = GUIWrapper; 89 | 90 | if (settings.datGui) { 91 | Cls = require('dat.gui').GUI; 92 | 93 | Cls.prototype.removeFolder = function(name) { 94 | var folder = this.__folders[name]; 95 | if (!folder) { 96 | return; 97 | } 98 | folder.close(); 99 | this.__ul.removeChild(folder.domElement.parentNode); 100 | delete this.__folders[name]; 101 | this.onResize(); 102 | }; 103 | } 104 | 105 | export const gui = new Cls(); 106 | export { GUIWrapper }; 107 | 108 | if (!detect.device.isDesktop) { 109 | Cls.toggleHide(); 110 | } 111 | -------------------------------------------------------------------------------- /src/webgl-app/lights/spot.js: -------------------------------------------------------------------------------- 1 | import { SpotLight } from 'three'; 2 | import { GUI } from 'dat.gui'; 3 | import settings from '../settings'; 4 | 5 | /** 6 | * Utility for creating spot lights 7 | * 8 | * @export 9 | * @class Spot 10 | */ 11 | export class Spot { 12 | settings: Object; 13 | light: SpotLight; 14 | gui: GUI; 15 | guiParent: GUI; 16 | 17 | constructor(options: Object = {}) { 18 | this.settings = Object.assign( 19 | { 20 | color: 0xd4d4d4, 21 | intensity: 0.6, 22 | distance: 100, 23 | angle: Math.PI / 3, 24 | power: Math.PI * 4, 25 | penumbra: 0, 26 | decay: 1, 27 | guiOpen: false 28 | }, 29 | options 30 | ); 31 | this.light = new SpotLight( 32 | this.settings.color, 33 | this.settings.intensity, 34 | this.settings.distance, 35 | this.settings.angle, 36 | this.settings.penumbra, 37 | this.settings.decay 38 | ); 39 | this.light.power = this.settings.power; 40 | this.light.position.set(1, 1, 1); 41 | } 42 | 43 | gui(guiParent: GUI) { 44 | this.guiParent = guiParent; 45 | this.gui = guiParent.addFolder('spot'); 46 | if (this.settings.guiOpen) this.gui.open(); 47 | const range = 100; 48 | this.gui.addColor(this.settings, 'color').onChange(this.onChange); 49 | this.gui.add(this.light, 'intensity', 0, 10, settings.guiPrecision); 50 | this.gui.add(this.light, 'distance', 0, 100); 51 | this.gui.add(this.light, 'decay', 0, 100); 52 | this.gui.add(this.light, 'angle', 0, 100); 53 | this.gui.add(this.light, 'penumbra', 0, 100); 54 | this.gui.add(this.light, 'power', 0, 100); 55 | this.gui 56 | .add(this.light.position, 'x', -range, range) 57 | .step(settings.guiPrecision) 58 | .name('position x'); 59 | this.gui 60 | .add(this.light.position, 'y', -range, range) 61 | .step(settings.guiPrecision) 62 | .name('position y'); 63 | this.gui 64 | .add(this.light.position, 'z', -range, range) 65 | .step(settings.guiPrecision) 66 | .name('position z'); 67 | } 68 | 69 | onChange = () => { 70 | this.light.color.setHex(this.settings.color); 71 | }; 72 | 73 | dispose() { 74 | this.guiParent.removeFolder(this.gui.name); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TweenLite } from 'gsap/gsap-core'; 3 | import './App.css'; 4 | import WebGLApp from './webgl-app/webgl-app'; 5 | import AppState from './webgl-app/app-state'; 6 | 7 | type Props = {}; 8 | 9 | type State = {| 10 | ready: boolean, 11 | windowSize: { width: number, height: number } 12 | |}; 13 | 14 | class App extends React.PureComponent { 15 | state = { 16 | ready: false, 17 | windowSize: { width: window.innerWidth, height: window.innerHeight } 18 | }; 19 | 20 | componentDidMount() { 21 | if (this.container === null) return; 22 | this.webglApp = new WebGLApp(this.container); 23 | this.webglApp 24 | .setup() 25 | .then(() => { 26 | this.webglApp.setState(new AppState(this.state)); 27 | this.webglApp.render(true); 28 | TweenLite.delayedCall(1, this.onReady); 29 | }) 30 | .catch((error: String) => { 31 | console.log(error); 32 | }); 33 | 34 | window.addEventListener('resize', this.onResize); 35 | } 36 | 37 | componentDidUpdate(prevProps: Object, prevState: Object) { 38 | if (this.container === null) return; 39 | 40 | this.webglApp.setState(new AppState(this.state)); 41 | 42 | if ( 43 | this.state.windowSize.width !== prevState.windowSize.width || 44 | this.state.windowSize.height !== prevState.windowSize.height 45 | ) { 46 | // Resize the app 47 | this.webglApp.resize(this.state.windowSize.width, this.state.windowSize.height); 48 | } 49 | } 50 | 51 | componentWillUnmount() { 52 | if (this.container === null) return; 53 | this.webglApp.render(false); 54 | window.removeEventListener('resize', this.onResize); 55 | } 56 | 57 | container: HTMLElement | null; 58 | webglApp: WebGLApp; 59 | 60 | onReady = () => { 61 | this.setState({ 62 | ready: true 63 | }); 64 | }; 65 | 66 | onResize = () => { 67 | this.setState({ 68 | windowSize: { width: window.innerWidth, height: window.innerHeight } 69 | }); 70 | }; 71 | 72 | render() { 73 | return ( 74 |
{ 77 | this.container = node; 78 | }} 79 | >
80 | ); 81 | } 82 | } 83 | 84 | export default App; 85 | -------------------------------------------------------------------------------- /src/webgl-app/utils/material-modifier.js: -------------------------------------------------------------------------------- 1 | import { UniformsUtils } from 'three'; 2 | 3 | const hooks = { 4 | vertex: { 5 | preTransform: 'before:#include \n', 6 | postTransform: 'after:#include \n', 7 | preNormal: 'before:#include \n' 8 | }, 9 | fragment: { 10 | preFragColor: 'before:gl_FragColor = vec4( outgoingLight, diffuseColor.a );\n', 11 | postFragColor: 'after:gl_FragColor = vec4( outgoingLight, diffuseColor.a );\n', 12 | postNormal: 'after:#include \n', 13 | postFragFog: 'after:#include \n' 14 | } 15 | }; 16 | 17 | function replace(shader: string, hooks: Object, config: Object) { 18 | Object.keys(hooks).forEach((hook: string) => { 19 | if (config[hook]) { 20 | const parts = hooks[hook].split(':'); 21 | const line = parts[1]; 22 | switch (parts[0]) { 23 | case 'after': { 24 | shader = shader.replace( 25 | line, 26 | `${line} 27 | ${config[hook]}` 28 | ); 29 | break; 30 | } 31 | default: { 32 | // before 33 | shader = shader.replace( 34 | line, 35 | `${config[hook]} 36 | ${line}` 37 | ); 38 | break; 39 | } 40 | } 41 | } 42 | }); 43 | return shader; 44 | } 45 | 46 | /** 47 | * The material modifier injects custom shader code and uniforms 48 | * to three's built in materials 49 | * 50 | * @export 51 | * @param {Object} shader 52 | * @param {Object} config 53 | * @returns 54 | */ 55 | export default function materialModifier(shader: Object, config: Object) { 56 | shader.uniforms = UniformsUtils.merge([shader.uniforms, config.uniforms]); 57 | 58 | shader.vertexShader = ` 59 | ${config.vertexShader.uniforms} 60 | ${config.vertexShader.functions} 61 | ${shader.vertexShader} 62 | `; 63 | shader.fragmentShader = ` 64 | ${config.fragmentShader.uniforms} 65 | ${config.fragmentShader.functions} 66 | ${shader.fragmentShader} 67 | `; 68 | 69 | // Injection 70 | shader.vertexShader = replace(shader.vertexShader, hooks.vertex, config.vertexShader); 71 | shader.fragmentShader = replace(shader.fragmentShader, hooks.fragment, config.fragmentShader); 72 | 73 | return shader; 74 | } 75 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/landing/objects/jam3/jam3.js: -------------------------------------------------------------------------------- 1 | import { GUI } from 'dat.gui'; 2 | import { Mesh, Group, ShaderMaterial, WebGLRenderTarget, Vector2, Vector3, PerspectiveCamera, Scene } from 'three'; 3 | import assetManager from '../../../../loading/asset-manager'; 4 | import { vertexShader, fragmentShader } from './shader.glsl'; 5 | import { getRenderBufferSize } from '../../../../rendering/resize'; 6 | 7 | export default class Jam3 { 8 | shader: Object; 9 | mesh: Mesh; 10 | group: Group; 11 | material: ShaderMaterial; 12 | gui: GUI; 13 | 14 | constructor(gui: GUI, particleMap: WebGLRenderTarget) { 15 | this.gui = gui.addFolder('Jam3'); 16 | this.gui.open(); 17 | this.group = new Group(); 18 | 19 | // Setup material 20 | const { width, height } = getRenderBufferSize(); 21 | this.material = new ShaderMaterial({ 22 | uniforms: { 23 | particleMap: { 24 | value: particleMap.texture 25 | }, 26 | resolution: { 27 | value: new Vector2(width, height) 28 | }, 29 | cameraPosition: { 30 | value: new Vector3(1, 1, 1) 31 | }, 32 | fresnelPow: { 33 | value: 25 34 | } 35 | }, 36 | vertexShader, 37 | fragmentShader 38 | }); 39 | 40 | const asset = assetManager.get('landing', 'jam3-logo'); 41 | 42 | // Make sure asset exists 43 | if (typeof asset === 'object' && asset !== null) { 44 | const scene: Scene = asset.scene; 45 | const model: Mesh = scene.children[0]?.children[0]; 46 | const scale = 300; 47 | model.scale.set(scale, scale, scale); 48 | 49 | model.children.forEach((mesh: Mesh) => { 50 | mesh.material = this.material; 51 | }); 52 | 53 | this.group.add(model); 54 | } 55 | 56 | // Gui controls 57 | this.gui.add(this.material.uniforms.fresnelPow, 'value', 0, 50).name('fresnelPow'); 58 | } 59 | 60 | /** 61 | * Resize handler 62 | * 63 | * @memberof Jam3 64 | */ 65 | resize() { 66 | const { width, height } = getRenderBufferSize(); 67 | this.material.uniforms.resolution.value.set(width, height); 68 | } 69 | 70 | /** 71 | * Update handler 72 | * 73 | * @param {PerspectiveCamera} camera 74 | * @memberof Jam3 75 | */ 76 | update(camera: PerspectiveCamera) { 77 | this.material.uniforms.cameraPosition.value.copy(camera.position); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/landing/landing-scene.js: -------------------------------------------------------------------------------- 1 | import BaseScene from '../base/base-scene'; 2 | import { VECTOR_ZERO } from '../../utils/math'; 3 | import assets from './assets'; 4 | import Background from '../../objects/background/background'; 5 | import renderer from '../../rendering/renderer'; 6 | import ParticlesNormal from './objects/particles/particles-normal'; 7 | import Particles from './objects/particles/particles'; 8 | import Jam3 from './objects/jam3/jam3'; 9 | import settings from '../../settings'; 10 | 11 | export const LANDING_SCENE_ID = 'landing'; 12 | 13 | export default class LandingScene extends BaseScene { 14 | constructor() { 15 | settings.devCamera = false; 16 | super({ id: LANDING_SCENE_ID, assets, gui: true, guiOpen: true, controls: true }); 17 | this.cameras.main.position.set(0, 0, 60); 18 | this.cameras.main.lookAt(VECTOR_ZERO); 19 | this.controls.main.enableDamping = true; 20 | } 21 | 22 | /** 23 | * Create and setup any objects for the scene 24 | * 25 | * @memberof LandingScene 26 | */ 27 | async createSceneObjects() { 28 | await new Promise((resolve, reject) => { 29 | try { 30 | this.background = new Background(this.gui, 100); 31 | this.scene.add(this.background.mesh); 32 | 33 | // Create particle classes 34 | this.particlesNormal = new ParticlesNormal(renderer); 35 | this.particles = new Particles( 36 | this.gui, 37 | 5000, // total particles 38 | this.particlesNormal, // particles normal texture class 39 | renderer.getPixelRatio() 40 | ); 41 | 42 | // Create Jam3 logo 43 | this.jam3 = new Jam3(this.gui, this.particles.renderTarget); 44 | this.scene.add(this.jam3.group); 45 | 46 | resolve(); 47 | } catch (error) { 48 | reject(error); 49 | } 50 | }); 51 | } 52 | 53 | /** 54 | * Resize the camera's projection matrix 55 | * 56 | * @memberof LandingScene 57 | */ 58 | resize = (width: number, height: number) => { 59 | this.cameras.dev.aspect = width / height; 60 | this.cameras.dev.updateProjectionMatrix(); 61 | this.cameras.main.aspect = width / height; 62 | this.cameras.main.updateProjectionMatrix(); 63 | this.particles.resize(); 64 | this.jam3.resize(); 65 | }; 66 | 67 | /** 68 | * Update loop 69 | * 70 | * @memberof LandingScene 71 | */ 72 | update = (delta: number) => { 73 | this.controls.main.update(); 74 | this.particlesNormal.render(this.camera); 75 | this.particles.render(delta, this.camera); 76 | this.jam3.update(this.camera); 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/webgl-app/utils/render-target-helper.js: -------------------------------------------------------------------------------- 1 | import { WebGLRenderTarget } from 'three'; 2 | import createCanvas from './canvas'; 3 | import renderer from '../rendering/renderer'; 4 | 5 | export default class RenderTargetHelper { 6 | renderTarget: WebGLRenderTarget; 7 | canvas: HTMLCanvasElement; 8 | ctx: CanvasRenderingContext2D; 9 | canvasFlipped: HTMLCanvasElement; 10 | ctxFlipped: CanvasRenderingContext2D; 11 | pixelBuffer: Uint8Array; 12 | imageData: ImageData; 13 | 14 | constructor(renderTarget: WebGLRenderTarget, options: Object = {}) { 15 | this.renderTarget = renderTarget; 16 | 17 | const { canvas, ctx } = createCanvas(renderTarget.width, renderTarget.height); 18 | const { canvas: canvasFlipped, ctx: ctxFlipped } = createCanvas(renderTarget.width, renderTarget.height); 19 | this.canvas = canvas; 20 | this.ctx = ctx; 21 | this.canvasFlipped = canvasFlipped; 22 | this.ctxFlipped = ctxFlipped; 23 | this.imageData = this.ctxFlipped.createImageData(this.canvas.width, this.canvas.height); 24 | this.pixelBuffer = new Uint8Array(this.renderTarget.width * this.renderTarget.height * 4); 25 | 26 | Object.assign(canvas.style, { 27 | position: 'absolute', 28 | zIndex: '1000', 29 | border: '1px solid white', 30 | pointerEvents: 'none', 31 | width: `${renderTarget.width}px`, 32 | height: `${renderTarget.height}px` 33 | }); 34 | this.setCssPosition(options); 35 | 36 | if (document.body) document.body.appendChild(this.canvas); 37 | } 38 | 39 | resize(width: number, height: number) { 40 | this.canvas.width = width; 41 | this.canvas.height = height; 42 | this.canvasFlipped.width = width; 43 | this.canvasFlipped.height = height; 44 | this.renderTarget.setSize(this.canvas.width, this.canvas.height); 45 | this.imageData = this.ctxFlipped.createImageData(this.canvas.width, this.canvas.height); 46 | this.pixelBuffer = new Uint8Array(this.renderTarget.width * this.renderTarget.height * 4); 47 | this.canvas.style.width = `${this.renderTarget.width / 2}px`; 48 | this.canvas.style.height = `${this.renderTarget.height / 2}px`; 49 | } 50 | 51 | setCssPosition(style: Object) { 52 | this.canvas.style.top = `${style.top / 2 || 0}px`; 53 | this.canvas.style.left = `${style.left || 0}px`; 54 | } 55 | 56 | update() { 57 | renderer.readRenderTargetPixels( 58 | this.renderTarget, 59 | 0, 60 | 0, 61 | this.renderTarget.width, 62 | this.renderTarget.height, 63 | this.pixelBuffer 64 | ); 65 | this.imageData.data.set(this.pixelBuffer); 66 | this.ctxFlipped.putImageData(this.imageData, 0, 0); 67 | this.ctx.save(); 68 | this.ctx.scale(1, -1); 69 | this.ctx.drawImage(this.canvasFlipped, 0, -this.canvas.height, this.canvas.width, this.canvas.height); 70 | this.ctx.restore(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/webgl-app/utils/render-stats.js: -------------------------------------------------------------------------------- 1 | import { WebGLRenderer } from 'three'; 2 | import settings from '../settings'; 3 | 4 | /** 5 | * @author mrdoob / http://mrdoob.com/ 6 | * @author jetienne / http://jetienne.com/ 7 | */ 8 | 9 | /** 10 | * Provide info on THREE.WebGLRenderer 11 | * 12 | * @param {Object} renderer the renderer to update 13 | * @param {Object} Camera the camera to update 14 | */ 15 | const RendererStats = function() { 16 | const container = document.createElement('div'); 17 | container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer;z-index:100000;top:48px;position:absolute;'; 18 | 19 | const msDiv = document.createElement('div'); 20 | msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:rgb(0, 0, 0);'; 21 | container.appendChild(msDiv); 22 | 23 | const msText = document.createElement('div'); 24 | msText.style.cssText = 25 | 'color:rgb(255, 255, 255);font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 26 | msText.innerHTML = 'WebGLRenderer'; 27 | msDiv.appendChild(msText); 28 | 29 | const msTexts = []; 30 | const nLines = 9; 31 | for (var i = 0; i < nLines; i++) { 32 | msTexts[i] = document.createElement('div'); 33 | msTexts[i].style.cssText = 34 | 'color:rgb(255, 255, 255);background-color:rgb(0, 0, 0);font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 35 | msDiv.appendChild(msTexts[i]); 36 | msTexts[i].innerHTML = '-'; 37 | } 38 | 39 | let lastTime = Date.now(); 40 | return { 41 | domElement: container, 42 | 43 | update: function(webglRenderer: WebGLRenderer) { 44 | // sanity check 45 | console.assert(webglRenderer instanceof WebGLRenderer); 46 | 47 | // refresh only 30time per second 48 | if (Date.now() - lastTime < 1000 / 30) return; 49 | lastTime = Date.now(); 50 | 51 | msTexts[0].textContent = '=== Memory ==='; 52 | msTexts[1].textContent = 'Programs: ' + webglRenderer.info.programs.length; 53 | msTexts[2].textContent = 'Geometries: ' + webglRenderer.info.memory.geometries; 54 | msTexts[3].textContent = 'Textures: ' + webglRenderer.info.memory.textures; 55 | msTexts[4].textContent = '=== Render ==='; 56 | msTexts[5].textContent = 'Calls: ' + webglRenderer.info.render.calls; 57 | msTexts[6].textContent = 'Triangles: ' + webglRenderer.info.render.triangles; 58 | msTexts[7].textContent = 'Lines: ' + webglRenderer.info.render.lines; 59 | msTexts[8].textContent = 'Points: ' + webglRenderer.info.render.points; 60 | } 61 | }; 62 | }; 63 | 64 | export function RenderStatsWrapper() { 65 | return { 66 | domElement: document.createElement('div'), 67 | update: (renderer: WebGLRenderer) => {} 68 | }; 69 | } 70 | 71 | const Cls = settings.isDevelopment ? RendererStats : RenderStatsWrapper; 72 | 73 | export default Cls; 74 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/post-processing/passes/film.glsl.js: -------------------------------------------------------------------------------- 1 | import { GUI } from 'dat.gui'; 2 | import { Material } from 'three'; 3 | import { rand, PI } from '../../../shaders/math.glsl'; 4 | 5 | export const uniforms = { 6 | filmEnabled: { value: 1 }, 7 | filmNoiseIntensity: { value: 0.35 }, 8 | filmScanIntensity: { value: 0.05 }, 9 | filmScanCount: { value: 4096 }, 10 | filmGrayscale: { value: 0 } 11 | }; 12 | 13 | export const fragmentUniforms = ` 14 | uniform bool filmEnabled; 15 | uniform bool filmGrayscale; 16 | uniform float filmNoiseIntensity; 17 | uniform float filmScanIntensity; 18 | uniform float filmScanCount; 19 | `; 20 | 21 | /** 22 | * @author alteredq / http://alteredqualia.com/ 23 | * 24 | * Film grain & scanlines shader 25 | * 26 | * - ported from HLSL to WebGL / GLSL 27 | * http://www.truevision3d.com/forums/showcase/staticnoise_colorblackwhite_scanline_shaders-t18698.0.html 28 | * 29 | * Screen Space Static Postprocessor 30 | * 31 | * Produces an analogue noise overlay similar to a film grain / TV static 32 | * 33 | * Original implementation and noise algorithm 34 | * Pat 'Hawthorne' Shearon 35 | * 36 | * Optimized scanlines + noise version with intensity scaling 37 | * Georg 'Leviathan' Steinrohder 38 | * 39 | * This version is provided under a Creative Commons Attribution 3.0 License 40 | * http://creativecommons.org/licenses/by/3.0/ 41 | */ 42 | 43 | export const fragmentPass = ` 44 | ${PI} 45 | ${rand} 46 | 47 | vec3 filmPass(vec3 outgoingColor, vec2 uv) { 48 | // Make some noise 49 | float dx = rand(uv + time); 50 | 51 | // Add noise 52 | vec3 cResult = outgoingColor.rgb + outgoingColor.rgb * clamp(0.1 + dx, 0.0, 1.0); 53 | 54 | // Get us a sine and cosine 55 | vec2 sc = vec2(sin(uv.y * filmScanCount), cos(uv.y * filmScanCount)); 56 | 57 | // Add scanlines 58 | cResult += outgoingColor.rgb * vec3(sc.x, sc.y, sc.x) * filmScanIntensity; 59 | 60 | // Interpolate between source and result by intensity 61 | cResult = outgoingColor.rgb + clamp(filmNoiseIntensity, 0.0,1.0) * (cResult - outgoingColor.rgb); 62 | 63 | // Convert to grayscale if desired 64 | if (filmGrayscale) { 65 | cResult = vec3( cResult.r * 0.3 + cResult.g * 0.59 + cResult.b * 0.11 ); 66 | } 67 | 68 | return cResult; 69 | } 70 | `; 71 | 72 | export const fragmentMain = ` 73 | // Film pass start 74 | if (filmEnabled) { 75 | outgoingColor.rgb = filmPass(outgoingColor.rgb, uv); 76 | } 77 | // Film pass end 78 | ` 79 | 80 | export function guiControls(gui: GUI, material: Material) { 81 | const guiPass = gui.addFolder('film pass'); 82 | guiPass.open(); 83 | guiPass.add(material.uniforms.filmEnabled, 'value', 0, 1, 1).name('enabled'); 84 | guiPass.add(material.uniforms.filmNoiseIntensity, 'value', 0, 1).name('noise intensity'); 85 | guiPass.add(material.uniforms.filmScanIntensity, 'value', 0, 1).name('scan intensity'); 86 | guiPass.add(material.uniforms.filmScanCount, 'value', 0, 4096).name('scan count'); 87 | guiPass.add(material.uniforms.filmGrayscale, 'value', 0, 1, 1).name('gayscale'); 88 | } 89 | -------------------------------------------------------------------------------- /src/webgl-app/loading/loaders/group-loader.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import detect from '@jam3/detect'; 3 | import Asset from '../asset'; 4 | import Loader from './loader'; 5 | import ImageLoader from './image-loader'; 6 | import JsonLoader from './json-loader'; 7 | import ThreeTextureLoader from './three-texture-loader'; 8 | import ThreeFBXLoader from './three-fbx-loader'; 9 | import ThreeGLTFLoader from './three-gltf-loader'; 10 | 11 | const LOADERS = { 12 | [Loader.image]: ImageLoader, 13 | [Loader.json]: JsonLoader, 14 | [Loader.threeTexture]: ThreeTextureLoader, 15 | [Loader.threeFBX]: ThreeFBXLoader, 16 | [Loader.threeGLTF]: ThreeGLTFLoader 17 | }; 18 | 19 | /** 20 | * Group loader loads an array of assets based on their asset types 21 | * 22 | * @export 23 | * @class GroupLoader 24 | * @extends {EventEmitter} 25 | */ 26 | export default class GroupLoader extends EventEmitter { 27 | constructor(options: Object = {}) { 28 | super(); 29 | this.id = options.id || ''; 30 | this.minParallel = options.minParallel || 5; 31 | this.maxParallel = options.maxParallel || 10; 32 | // How many parallel loads at once 33 | this.parallelLoads = detect.device.isDesktop ? this.maxParallel : this.minParallel; 34 | } 35 | 36 | load = (manifest: Asset[]) => { 37 | this.loaders = []; 38 | 39 | manifest.forEach(asset => { 40 | if (LOADERS[asset.type] !== undefined) { 41 | this.loaders.push(new LOADERS[asset.type](asset)); 42 | } 43 | }); 44 | 45 | this.loaded = 0; 46 | this.queue = 0; 47 | this.currentParallel = 0; 48 | this.total = this.loaders.length; 49 | 50 | if (this.total === 0) { 51 | this.emit('loaded', manifest); 52 | } else { 53 | this.loadNextInQueue(); 54 | } 55 | }; 56 | 57 | /** 58 | * Load the next in queue 59 | * 60 | * @memberof GroupLoader 61 | */ 62 | loadNextInQueue = () => { 63 | if (this.queue < this.total) { 64 | if (this.currentParallel < this.parallelLoads) { 65 | const loader = this.loaders[this.queue]; 66 | this.queue += 1; 67 | this.currentParallel += 1; 68 | loader.once('loaded', this.onLoaded); 69 | loader.once('error', this.onError); 70 | loader.load(); 71 | this.loadNextInQueue(); 72 | } 73 | } 74 | }; 75 | 76 | /** 77 | * Loaded handler 78 | * 79 | * @memberof GroupLoader 80 | */ 81 | onLoaded = () => { 82 | this.loaded += 1; 83 | // console.log(`${this.id} loaded`, this.loaded, '/', this.total); 84 | this.emit('progress', this.loaded / this.total); 85 | if (this.loaded === this.total) { 86 | const assets = []; 87 | this.loaders.forEach((loader: Loader) => { 88 | assets.push(loader.asset); 89 | }); 90 | this.emit('loaded', assets); 91 | } else { 92 | this.currentParallel -= 1; 93 | this.loadNextInQueue(); 94 | } 95 | }; 96 | 97 | /** 98 | * Error handler 99 | * 100 | * @memberof GroupLoader 101 | */ 102 | onError = (error: string) => { 103 | this.emit('error', error); 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/preloader/preloader-scene.js: -------------------------------------------------------------------------------- 1 | import { TweenLite } from 'gsap/gsap-core'; 2 | import { Mesh, RingBufferGeometry, ShaderMaterial } from 'three'; 3 | import BaseScene from '../base/base-scene'; 4 | import { TWO_PI, VECTOR_ZERO } from '../../utils/math'; 5 | import settings from '../../settings'; 6 | 7 | export const PRELOADER_SCENE_ID = 'preloader'; 8 | 9 | export default class PreloaderScene extends BaseScene { 10 | constructor() { 11 | super({ id: PRELOADER_SCENE_ID }); 12 | this.camera.position.set(0, 0, 10); 13 | this.camera.lookAt(VECTOR_ZERO); 14 | } 15 | 16 | /** 17 | * Create and setup any objects for the scene 18 | * 19 | * @memberof PreloaderScene 20 | */ 21 | async createSceneObjects() { 22 | await new Promise((resolve, reject) => { 23 | try { 24 | // Create a spinner mesh to show loading progression 25 | this.spinner = new Mesh( 26 | new RingBufferGeometry(0.9, 1, 32, 1, 0, TWO_PI * 0.75), 27 | new ShaderMaterial({ 28 | transparent: true, 29 | uniforms: { 30 | opacity: { value: 0 } 31 | }, 32 | vertexShader: ` 33 | varying vec2 vUv; 34 | void main() { 35 | vUv = uv; 36 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 37 | } 38 | `, 39 | fragmentShader: ` 40 | uniform float opacity; 41 | varying vec2 vUv; 42 | void main() { 43 | gl_FragColor = vec4(vUv, 1.0, vUv.y * opacity); 44 | } 45 | ` 46 | }) 47 | ); 48 | this.spinner.name = 'spinner'; 49 | this.scene.add(this.spinner); 50 | this.animateInit(); 51 | resolve(); 52 | } catch (error) { 53 | reject(error); 54 | } 55 | }); 56 | } 57 | 58 | preloadGpuCullScene = (culled: boolean) => { 59 | this.spinner.material.uniforms.opacity.value = culled ? 1 : 0; 60 | }; 61 | 62 | animateInit = () => { 63 | TweenLite.killTweensOf(this.spinner.material.uniforms.opacity); 64 | this.spinner.material.uniforms.opacity.value = 0; 65 | }; 66 | 67 | async animateIn() { 68 | await new Promise((resolve, reject) => { 69 | if (settings.skipTransitions) { 70 | resolve(); 71 | return; 72 | } 73 | TweenLite.to(this.spinner.material.uniforms.opacity, 1, { 74 | value: 1, 75 | onComplete: () => { 76 | resolve(); 77 | } 78 | }); 79 | }); 80 | } 81 | 82 | async animateOut() { 83 | await new Promise((resolve, reject) => { 84 | if (settings.skipTransitions) { 85 | resolve(); 86 | return; 87 | } 88 | TweenLite.to(this.spinner.material.uniforms.opacity, 1, { 89 | value: 0, 90 | onComplete: () => { 91 | resolve(); 92 | } 93 | }); 94 | }); 95 | } 96 | 97 | /** 98 | * Update loop 99 | * 100 | * @memberof PreloaderScene 101 | */ 102 | update = (delta: number) => { 103 | this.spinner.rotation.z -= delta * 2; 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/interactive-sphere/objects/sphere/sphere.js: -------------------------------------------------------------------------------- 1 | import { TweenLite } from 'gsap/gsap-core'; 2 | import { Mesh, MeshLambertMaterial, SphereBufferGeometry, PerspectiveCamera } from 'three'; 3 | import materialModifier from '../../../../utils/material-modifier'; 4 | import shaderConfig from './shader.glsl'; 5 | import InteractiveObject from '../../../../interaction/interactive-object'; 6 | import { getGraphicsMode, GRAPHICS_HIGH } from '../../../../rendering/graphics'; 7 | 8 | export default class Sphere { 9 | camera: PerspectiveCamera; 10 | shader: Object; 11 | mesh: Mesh; 12 | interactiveObject: InteractiveObject; 13 | 14 | constructor(camera: PerspectiveCamera) { 15 | this.camera = camera; 16 | 17 | // Use less polys on normal graphics mode 18 | const divisions = getGraphicsMode() === GRAPHICS_HIGH ? 64 : 32; 19 | const geometry = new SphereBufferGeometry(1, divisions, divisions); 20 | const material = new MeshLambertMaterial({ transparent: true, opacity: 0 }); 21 | 22 | this.shader = undefined; 23 | let compiled = false; 24 | // Customise the lambert material 25 | material.onBeforeCompile = (shader: Object) => { 26 | if (compiled) return; 27 | compiled = true; 28 | this.shader = materialModifier(shader, shaderConfig); 29 | }; 30 | 31 | this.mesh = new Mesh(geometry, material); 32 | this.interactiveObject = new InteractiveObject(this.mesh, this.camera, { 33 | touchStart: true, 34 | touchMove: true, 35 | touchEnd: true, 36 | mouseMove: false 37 | }); 38 | this.interactiveObject.on('start', this.onStart); 39 | this.interactiveObject.on('hover', this.onHover); 40 | this.interactiveObject.on('end', this.onEnd); 41 | } 42 | 43 | onStart = (event: Object) => { 44 | // console.log('start', event); 45 | this.scaleMesh(true); 46 | }; 47 | 48 | onHover = (over: boolean, event: Object) => { 49 | // console.log(over ? 'over' : 'out', over ? event : ''); 50 | }; 51 | 52 | onEnd = () => { 53 | // console.log('end'); 54 | this.scaleMesh(false); 55 | }; 56 | 57 | preloadGpuCullScene = (culled: boolean) => { 58 | this.mesh.material.opacity = culled ? 1 : 0; 59 | }; 60 | 61 | animateInit = () => { 62 | TweenLite.killTweensOf(this.mesh.material.opacity); 63 | this.mesh.material.opacity = 0; 64 | }; 65 | 66 | async animateIn() { 67 | await new Promise((resolve, reject) => { 68 | TweenLite.to(this.mesh.material, 1, { 69 | opacity: 1, 70 | onComplete: () => { 71 | resolve(); 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | async animateOut() { 78 | await new Promise((resolve, reject) => { 79 | TweenLite.to(this.mesh.material, 1, { 80 | opacity: 0, 81 | onComplete: () => { 82 | resolve(); 83 | } 84 | }); 85 | }); 86 | } 87 | 88 | scaleMesh = (over: boolean) => { 89 | TweenLite.killTweensOf(this.mesh.scale); 90 | TweenLite.to(this.mesh.scale, 0.5, { 91 | x: over ? 1.6 : 1, 92 | y: over ? 1.6 : 1, 93 | z: over ? 1.6 : 1 94 | }); 95 | }; 96 | 97 | /** 98 | * Update loop 99 | * 100 | * @param {Number} delta 101 | * @memberof Sphere 102 | */ 103 | update(delta: number) { 104 | if (this.shader) { 105 | this.shader.uniforms.time.value += delta; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/camera-transitions/camera-transitions-scene.js: -------------------------------------------------------------------------------- 1 | import { CameraHelper } from 'three'; 2 | import { Power1 } from 'gsap'; 3 | import BaseScene from '../base/base-scene'; 4 | import assets from './assets'; 5 | import CameraDollyManager from '../../cameras/camera-dolly/camera-dolly-manager'; 6 | import { resetCamera } from '../../cameras/cameras'; 7 | import settings from '../../settings'; 8 | 9 | export const CAMERA_TRANSITION_SCENE_ID = 'camera-transitions'; 10 | 11 | export default class CameraTransitionsScene extends BaseScene { 12 | constructor() { 13 | // Show dev camera view during this scene 14 | settings.devCamera = true; 15 | super({ id: CAMERA_TRANSITION_SCENE_ID, assets, gui: true, guiOpen: true, controls: true }); 16 | resetCamera(this.cameras.dev, 50); 17 | } 18 | 19 | /** 20 | * Create and setup any objects for the scene 21 | * 22 | * @memberof CameraTransitionsScene 23 | */ 24 | async createSceneObjects() { 25 | await new Promise((resolve, reject) => { 26 | try { 27 | // Disable main control sincw we're using the camera dolly 28 | this.controls.main.enabled = false; 29 | 30 | this.gui.add(this, 'play'); 31 | this.gui.add(this, 'stop'); 32 | 33 | // Create a camera helper to see the main camera easier 34 | const helper = new CameraHelper(this.cameras.main); 35 | this.scene.add(helper); 36 | 37 | // Require camera dolly tracks 38 | const tracks = { 39 | 'track 0': require('./data/dolly-data-0.json'), 40 | 'track 1': require('./data/dolly-data-1.json') 41 | }; 42 | 43 | this.trackIds = Object.keys(tracks); 44 | this.trackIndex = 1; 45 | 46 | // Create camera dolly manager 47 | this.cameraDollyManager = new CameraDollyManager({ 48 | gui: this.gui, 49 | guiOpen: true 50 | }); 51 | this.scene.add(this.cameraDollyManager.group); 52 | 53 | // Add tracks to the manager 54 | Object.keys(tracks).forEach((id: string) => { 55 | this.cameraDollyManager.addTransition( 56 | id, 57 | tracks[id], 58 | this.cameras.main, 59 | this.cameras.dev, 60 | this.controls.dev, 61 | { 62 | linesVisible: true, 63 | controlsVisible: false, 64 | pointsVisible: true 65 | } 66 | ); 67 | }); 68 | 69 | this.play(); 70 | 71 | resolve(); 72 | } catch (error) { 73 | reject(error); 74 | } 75 | }); 76 | } 77 | 78 | /** 79 | * Play the current camera dolly track 80 | * 81 | * @memberof CameraTransitionsScene 82 | */ 83 | play = () => { 84 | this.animateCamera(); 85 | }; 86 | 87 | /** 88 | * Stop the current camera dolly track 89 | * 90 | * @memberof CameraTransitionsScene 91 | */ 92 | stop = () => { 93 | this.cameraDollyManager.stop(); 94 | }; 95 | 96 | /** 97 | * Cycle through camera dolly tracks 98 | * 99 | * @memberof CameraTransitionsScene 100 | */ 101 | animateCamera() { 102 | this.cameraDollyManager.setTransition(this.trackIds[this.trackIndex], this.cameras.main); 103 | this.cameraDollyManager.transition(5, Power1.easeOut).then(() => { 104 | this.trackIndex++; 105 | this.trackIndex %= this.trackIds.length; 106 | this.animateCamera(); 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-react-app", 3 | "version": "0.2.0", 4 | "description": "Jam3's WebGL React App Boilerplate", 5 | "keywords": ["webgl", "threejs", "best practices", "create react app"], 6 | "private": true, 7 | "author": { 8 | "name": "Amelie Maia Rosser", 9 | "email": "amelierosser1986@gmail.com", 10 | "url": "https://ameliemaia.com/" 11 | }, 12 | "contributors": [ 13 | {"name":"Iran Reyes", "email":"iran.reyes@jam3.com"}, 14 | {"name":"Peter Altamirano", "email":"peter.altamirano@jam3.com"} 15 | ], 16 | "homepage": "https://jam3.github.io/webgl-react-app/", 17 | "bugs": { 18 | "url": "https://github.com/Jam3/webgl-react-app/issues" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/Jam3/webgl-react-app.git" 23 | }, 24 | "license": "MIT", 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "npm run linters && react-scripts build", 28 | "deploy": "env-cmd -f .env.gh-pages npm run build && gh-pages -d build", 29 | "eject": "react-scripts eject", 30 | "audit": "audit-ci --high", 31 | "js-lint": "eslint './src/**/*.js' -c ./.eslintrc.json --quiet --ignore-pattern .gitignore", 32 | "flow": "flow", 33 | "flow-typed": "flow-typed install", 34 | "linters": "npm-run-all flow js-lint", 35 | "assets": "node scripts/assets/optimise", 36 | "postinstall": "npm run flow-typed" 37 | }, 38 | "engines": { 39 | "node": ">=13.7.0", 40 | "npm": ">=6.13.6" 41 | }, 42 | "dependencies": { 43 | "@jam3/detect": "^1.0.2", 44 | "@jam3/stats": "^1.0.1", 45 | "dat.gui": "^0.7.7", 46 | "detect-gpu": "^1.2.0", 47 | "env-cmd": "^10.1.0", 48 | "eventemitter3": "^4.0.0", 49 | "file-saver": "^2.0.2", 50 | "gsap": "^3.2.6", 51 | "gsap-promisify": "^1.0.2", 52 | "query-string": "^6.12.1", 53 | "react": "^16.13.1", 54 | "react-dom": "^16.13.1", 55 | "react-scripts": "3.4.0", 56 | "three": "^0.113.2" 57 | }, 58 | "devDependencies": { 59 | "@commitlint/cli": "^8.3.5", 60 | "audit-ci": "^2.5.1", 61 | "eslint-config-prettier": "^6.11.0", 62 | "eslint-config-react-jam3": "^1.1.0", 63 | "eslint-plugin-jam3": "^0.2.3", 64 | "eslint-plugin-prettier": "^3.1.3", 65 | "eslint-plugin-promise": "^4.2.1", 66 | "fbx2gltf": "^0.9.7-p1", 67 | "file-extension": "^4.0.5", 68 | "file-name": "^0.1.0", 69 | "flow-bin": "^0.118.0", 70 | "flow-typed": "^3.1.0", 71 | "gh-pages": "^2.2.0", 72 | "husky": "^4.2.5", 73 | "image-size": "^0.8.3", 74 | "lint-staged": "^10.1.7", 75 | "npm-run-all": "^4.1.5", 76 | "prettier": "^1.19.1", 77 | "sharp": "^0.24.1", 78 | "shelljs": "^0.8.3" 79 | }, 80 | 81 | "eslintConfig": { 82 | "extends": "react-app" 83 | }, 84 | "browserslist": { 85 | "production": [ 86 | ">0.2%", 87 | "not dead", 88 | "not op_mini all" 89 | ], 90 | "development": [ 91 | "last 1 chrome version", 92 | "last 1 firefox version", 93 | "last 1 safari version" 94 | ] 95 | }, 96 | "lint-staged": { 97 | "src/**/*.js": [ 98 | "prettier --write" 99 | ] 100 | }, 101 | "husky": { 102 | "hooks": { 103 | "pre-commit": "echo 'Pre-commit checks...' && npm run linters && lint-staged", 104 | "pre-push": "echo 'Pre-push checks...' && npm run linters && npm run audit", 105 | "commitmsg": "commitlint -e $GIT_PARAMS" 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /scripts/assets/model-optimiser.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const shell = require('shelljs'); 3 | const convert = require('fbx2gltf'); 4 | const fileExtension = require('file-extension'); 5 | const configTemplate = require('./config').models; 6 | 7 | /* 8 | * Convert or copy models from 9 | * the source to destination directory 10 | * 11 | * Preview your glb files with: https://gltf-viewer.donmccurdy.com/ 12 | * fbx2gltf plugin: https://www.npmjs.com/package/fbx2gltf 13 | * GLTFLoader documentation: https://threejs.org/docs/#examples/en/loaders/GLTFLoader 14 | */ 15 | module.exports = class ModelOptimiser { 16 | constructor() { 17 | this.files = []; 18 | } 19 | 20 | add(directory, name, extension) { 21 | const fileName = `${name}.${extension}`; 22 | const filePath = `${directory}/${fileName}`; 23 | this.files.push({ filePath, fileName, name, extension }); 24 | } 25 | 26 | includes(file) { 27 | return /(obj|fbx|glb|gltf)$/i.test(file); 28 | } 29 | 30 | copy(file, fileDest) { 31 | return new Promise((resolve, reject) => { 32 | const output = shell.cp('-f', file, fileDest); 33 | if (output.code === 0) { 34 | resolve(); 35 | } else { 36 | reject(output.stderr); 37 | } 38 | }); 39 | } 40 | 41 | convert(file, fileDest) { 42 | return new Promise((resolve, reject) => { 43 | convert(file, fileDest, ['--khr-materials-unlit', '--draco', '--verbose', '--no-flip-v']).then( 44 | destPath => { 45 | resolve(); 46 | }, 47 | error => { 48 | reject(error); 49 | } 50 | ); 51 | }); 52 | } 53 | 54 | validateConfig(configFile) { 55 | return configFile.models !== undefined; 56 | } 57 | 58 | process(folderName, srcDirectory, destDirectory) { 59 | return new Promise((resolve, reject) => { 60 | try { 61 | if (this.files.length === 0) { 62 | resolve(); 63 | return; 64 | } 65 | 66 | const tmpDirectory = `${srcDirectory}/tmp-models`; 67 | shell.mkdir('-p', tmpDirectory); 68 | 69 | // Check of the current directory includes a config file 70 | let config = {}; 71 | try { 72 | const file = Object.assign(config, JSON.parse(fs.readFileSync(`${srcDirectory}/config.json`))); 73 | if (this.validateConfig(file)) config = file.models; 74 | } catch (error) {} 75 | 76 | const queue = []; 77 | this.files.forEach(data => { 78 | const fileConfig = config[data.fileName] || configTemplate; 79 | // Only convert models if the flag is true 80 | if (fileConfig.convert) { 81 | const fileDest = `${tmpDirectory}/${data.name}.glb`; 82 | queue.push(this.convert(data.filePath, fileDest)); 83 | } else { 84 | const fileDest = `${tmpDirectory}/`; 85 | queue.push(this.copy(data.filePath, fileDest)); 86 | } 87 | }); 88 | 89 | Promise.all(queue) 90 | .then(() => { 91 | // Create destination path 92 | const destinationDirectory = `${destDirectory}/${folderName}`; 93 | 94 | // Remove old files 95 | shell.ls(destinationDirectory).forEach(file => { 96 | const extension = fileExtension(file); 97 | if (this.includes(extension)) { 98 | const fullPath = `${destinationDirectory}/${file}`; 99 | shell.rm(fullPath); 100 | } 101 | }); 102 | 103 | // Make directory if it doesn't exist 104 | shell.mkdir('-p', destinationDirectory); 105 | 106 | // Copy new files to destination directory 107 | shell.cp(`${tmpDirectory}/*`, destinationDirectory); 108 | 109 | // Cleanup tmp directory 110 | shell.rm('-rf', tmpDirectory); 111 | 112 | resolve(); 113 | }) 114 | .catch(reject); 115 | } catch (error) { 116 | reject(error); 117 | } 118 | }); 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/post-processing/post-processing.js: -------------------------------------------------------------------------------- 1 | import { OrthographicCamera, WebGLRenderTarget } from 'three'; 2 | import { GUI } from 'dat.gui'; 3 | import { bigTriangle } from '../../utils/geometry'; 4 | import { createRenderTarget } from '../render-target'; 5 | import { getRenderBufferSize } from '../resize'; 6 | import TransitionPass from './passes/transition-pass/transition-pass'; 7 | import FinalPass from './passes/final-pass/final-pass'; 8 | import EmptyScene from '../../scenes/empty/empty-scene'; 9 | import renderer from '../renderer'; 10 | import settings from '../../settings'; 11 | import BaseScene from '../../scenes/base/base-scene'; 12 | 13 | export default class PostProcessing { 14 | gui: GUI; 15 | camera: OrthographicCamera; 16 | renderTargetA: WebGLRenderTarget; 17 | renderTargetB: WebGLRenderTarget; 18 | renderTargetC: WebGLRenderTarget; 19 | transitionPass: TransitionPass; 20 | finalPass: FinalPass; 21 | currentScene: BaseScene; 22 | lastPass: mixed; 23 | sceneA: BaseScene; 24 | sceneB: BaseScene; 25 | 26 | constructor(gui: GUI) { 27 | // Create gui 28 | this.gui = gui.addFolder('post processing'); 29 | // this.gui.open(); 30 | // Create big triangle geometry, faster than using quad 31 | const geometry = bigTriangle(); 32 | // Post camera 33 | this.camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1); 34 | // Setup render targets 35 | const { width, height } = getRenderBufferSize(); 36 | const options = { stencilBuffer: false }; 37 | this.renderTargetA = createRenderTarget(width, height, options); 38 | this.renderTargetB = createRenderTarget(width, height, options); 39 | this.renderTargetC = createRenderTarget(width, height, options); 40 | 41 | // Create passes 42 | this.transitionPass = new TransitionPass(this.gui, geometry, this.camera); 43 | this.finalPass = new FinalPass(this.gui, geometry, this.camera); 44 | 45 | // Create empty scenes 46 | const sceneA = new EmptyScene('post scene a', 0x000000); 47 | const sceneB = new EmptyScene('post scene b', 0x000000); 48 | sceneA.setup(); 49 | sceneB.setup(); 50 | 51 | this.setScenes(sceneA, sceneB); 52 | this.resize(); 53 | } 54 | 55 | /** 56 | * Set the two main scenes used for the transition pass 57 | * 58 | * @param {BaseScene} sceneA 59 | * @param {BaseScene} sceneB 60 | * @memberof PostProcessing 61 | */ 62 | setScenes(sceneA: BaseScene, sceneB: BaseScene) { 63 | this.sceneA = sceneA; 64 | this.sceneB = sceneB; 65 | } 66 | 67 | /** 68 | * Resize handler for passes and render targets 69 | * 70 | * @memberof PostProcessing 71 | */ 72 | resize() { 73 | const scale = settings.devCamera ? settings.viewportPreviewScale : 1; 74 | let { width, height } = getRenderBufferSize(); 75 | width *= scale; 76 | height *= scale; 77 | this.renderTargetA.setSize(width, height); 78 | this.renderTargetB.setSize(width, height); 79 | this.renderTargetC.setSize(width, height); 80 | this.transitionPass.resize(width, height); 81 | this.finalPass.resize(width, height); 82 | } 83 | 84 | /** 85 | * Render passes and output to screen 86 | * 87 | * @param {Number} delta 88 | * @memberof PostProcessing 89 | */ 90 | render(delta: number) { 91 | // Determine the current scene based on the transition pass value 92 | this.currentScene = this.transitionPass.mesh.material.uniforms.transition.value === 0 ? this.sceneA : this.sceneB; 93 | this.lastPass = this.currentScene; 94 | 95 | // If the transition pass is active 96 | if (this.transitionPass.active) { 97 | this.transitionPass.render(this.sceneA, this.sceneB, this.renderTargetA, this.renderTargetB, delta); 98 | this.lastPass = this.transitionPass; 99 | } else { 100 | // Otherwise we just render the current scene 101 | renderer.setClearColor(this.currentScene.clearColor); 102 | this.currentScene.update(delta); 103 | } 104 | 105 | // Render the final pass which contains all the post fx 106 | this.finalPass.render(this.lastPass, this.renderTargetC, delta); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/post-processing/passes/final-pass/final-pass.js: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | Mesh, 4 | ShaderMaterial, 5 | Vector2, 6 | UniformsUtils, 7 | WebGLRenderTarget, 8 | BufferGeometry, 9 | OrthographicCamera, 10 | PerspectiveCamera 11 | } from 'three'; 12 | import { GUI } from 'dat.gui'; 13 | import { vertexShader, fragmentShader } from './shader.glsl'; 14 | import { getRenderBufferSize } from '../../../resize'; 15 | import { uniforms as filmUniforms, guiControls as filmGuiControls } from '../../passes/film.glsl'; 16 | import { uniforms as fxaaUniforms, guiControls as fxaaGuiControls } from '../../passes/fxaa.glsl'; 17 | import renderer from '../../../renderer'; 18 | 19 | /** 20 | * The final pass contains the post fx and is then output to the screen 21 | * 22 | * @export 23 | * @class FinalPass 24 | */ 25 | export default class FinalPass { 26 | gui: GUI; 27 | scene: Scene; 28 | camera: OrthographicCamera; 29 | mesh: Mesh; 30 | 31 | constructor(gui: GUI, geometry: BufferGeometry, camera: OrthographicCamera) { 32 | // Create gui 33 | this.gui = gui.addFolder('final pass'); 34 | this.gui.open(); 35 | // Create scene 36 | this.scene = new Scene(); 37 | // Use camera from post processing 38 | this.camera = camera; 39 | const { width, height } = getRenderBufferSize(); 40 | // Setup shader and combine uniforms from any post fx you want to include 41 | const material = new ShaderMaterial({ 42 | uniforms: UniformsUtils.merge([ 43 | { 44 | time: { 45 | value: 0 46 | }, 47 | tDiffuse: { 48 | // Keep it the same as threejs for reusability 49 | value: null 50 | }, 51 | resolution: { 52 | value: new Vector2(width, height) 53 | } 54 | }, 55 | fxaaUniforms, 56 | filmUniforms 57 | ]), 58 | vertexShader, 59 | fragmentShader 60 | }); 61 | 62 | // Add gui controls 63 | fxaaGuiControls(this.gui, material); 64 | filmGuiControls(this.gui, material); 65 | 66 | // Create the mesh and turn off matrixAutoUpdate 67 | this.mesh = new Mesh(geometry, material); 68 | this.mesh.matrixAutoUpdate = false; 69 | this.mesh.updateMatrix(); 70 | this.scene.add(this.mesh); 71 | } 72 | 73 | /** 74 | * Resize handler, update uniforms 75 | * 76 | * @param {Number} width 77 | * @param {Number} height 78 | * @memberof FinalPass 79 | */ 80 | resize(width: number, height: number) { 81 | this.mesh.material.uniforms.resolution.value.x = width; 82 | this.mesh.material.uniforms.resolution.value.y = height; 83 | this.mesh.material.uniforms.fxaaResolution.value.x = 1 / width; 84 | this.mesh.material.uniforms.fxaaResolution.value.y = 1 / height; 85 | } 86 | 87 | /** 88 | * Render the pass and output to screen 89 | * 90 | * @param {*} scene 91 | * @param {WebGLRenderTarget} renderTarget 92 | * @param {Number} delta 93 | * @memberof FinalPass 94 | */ 95 | render(scene: Scene, renderTarget: WebGLRenderTarget, delta: number) { 96 | renderer.setRenderTarget(renderTarget); 97 | renderer.render(scene.scene, scene.camera); 98 | renderer.setRenderTarget(null); 99 | this.mesh.material.uniforms.tDiffuse.value = renderTarget.texture; 100 | this.mesh.material.uniforms.time.value += delta; 101 | renderer.render(this.scene, this.camera); 102 | } 103 | 104 | /** 105 | * Render the final pass when used with the screenshot utility 106 | * 107 | * @param {Scene} scene 108 | * @param {PerspectiveCamera} camera 109 | * @param {WebGLRenderTarget} renderTargetA 110 | * @param {WebGLRenderTarget} renderTargetB 111 | * @param {number} delta 112 | * @memberof FinalPass 113 | */ 114 | screenshotRender( 115 | scene: Scene, 116 | camera: PerspectiveCamera, 117 | renderTargetA: WebGLRenderTarget, 118 | renderTargetB: WebGLRenderTarget, 119 | delta: number 120 | ) { 121 | this.mesh.material.uniforms.tDiffuse.value = renderTargetA.texture; 122 | this.mesh.material.uniforms.time.value += delta; 123 | renderer.setRenderTarget(renderTargetB); 124 | renderer.render(this.scene, this.camera); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /scripts/assets/texture-optimiser.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const shell = require('shelljs'); 3 | const sharp = require('sharp'); 4 | const sizeOf = require('image-size'); 5 | const fileExtension = require('file-extension'); 6 | const configTemplate = require('./config').textures; 7 | 8 | /* 9 | * Resize and copy textures from 10 | * the source to destination directory 11 | * 12 | * Uses sharp for the image conversion: https://www.npmjs.com/package/sharp 13 | */ 14 | module.exports = class TextureOptimiser { 15 | constructor() { 16 | this.files = []; 17 | } 18 | 19 | add(directory, name, extension) { 20 | const fileName = `${name}.${extension}`; 21 | const filePath = `${directory}/${fileName}`; 22 | this.files.push({ filePath, fileName, name, extension }); 23 | } 24 | 25 | includes(file) { 26 | return /(jpg|png)$/i.test(file); 27 | } 28 | 29 | copy(file, fileDest) { 30 | return new Promise((resolve, reject) => { 31 | sharp(file).toFile(fileDest, (error, info) => { 32 | if (error) { 33 | reject(error); 34 | return; 35 | } 36 | resolve(fileDest); 37 | }); 38 | }); 39 | } 40 | 41 | resize(file, fileDest, size) { 42 | return new Promise((resolve, reject) => { 43 | const dimensions = sizeOf(file); 44 | const scale = dimensions.height / dimensions.width; 45 | sharp(file) 46 | .resize(size, Math.floor(size * scale)) 47 | .toFile(fileDest, (error, info) => { 48 | if (error) { 49 | reject(error); 50 | return; 51 | } 52 | resolve(fileDest); 53 | }); 54 | }); 55 | } 56 | 57 | validateConfig(configFile) { 58 | return configFile.models !== undefined; 59 | } 60 | 61 | process(folderName, srcDirectory, destDirectory) { 62 | return new Promise((resolve, reject) => { 63 | try { 64 | if (this.files.length === 0) { 65 | resolve(); 66 | return; 67 | } 68 | 69 | const tmpDirectory = `${srcDirectory}/tmp-textures`; 70 | shell.mkdir('-p', tmpDirectory); 71 | 72 | // Check of the current directory includes a config file 73 | let config = {}; 74 | try { 75 | const file = Object.assign(config, JSON.parse(fs.readFileSync(`${srcDirectory}/config.json`))); 76 | if (this.validateConfig(file)) config = file.textures; 77 | } catch (error) {} 78 | 79 | const queue = []; 80 | this.files.forEach(data => { 81 | const fileConfig = config[data.fileName] || configTemplate; 82 | // Only resize textures if a size is specified and the resize flag is true 83 | if (fileConfig.sizes.length > 0 && fileConfig.resize) { 84 | fileConfig.sizes.forEach(size => { 85 | const fileDest = `${tmpDirectory}/${data.name}-${size}.${data.extension}`; 86 | queue.push(this.resize(data.filePath, fileDest, size)); 87 | }); 88 | } else { 89 | const fileDest = `${tmpDirectory}/${data.fileName}`; 90 | queue.push(this.copy(data.filePath, fileDest)); 91 | } 92 | }); 93 | 94 | Promise.all(queue) 95 | .then(() => { 96 | // Create destination path 97 | const destinationDirectory = `${destDirectory}/${folderName}`; 98 | 99 | // Remove old files 100 | shell.ls(destinationDirectory).forEach(file => { 101 | const extension = fileExtension(file); 102 | if (this.includes(extension)) { 103 | const fullPath = `${destinationDirectory}/${file}`; 104 | shell.rm(fullPath); 105 | } 106 | }); 107 | 108 | // Make directory if it doesn't exist 109 | shell.mkdir('-p', destinationDirectory); 110 | 111 | // Copy new files to destination directory 112 | shell.cp(`${tmpDirectory}/*`, destinationDirectory); 113 | 114 | // Cleanup tmp directory 115 | shell.rm('-rf', tmpDirectory); 116 | 117 | resolve(); 118 | }) 119 | .catch(reject); 120 | } catch (error) { 121 | reject(error); 122 | } 123 | }); 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /src/webgl-app/rendering/post-processing/passes/transition-pass/transition-pass.js: -------------------------------------------------------------------------------- 1 | import { Scene, Mesh, ShaderMaterial, Vector2, BufferGeometry, OrthographicCamera, WebGLRenderTarget } from 'three'; 2 | import { TweenLite } from 'gsap'; 3 | import { GUI } from 'dat.gui'; 4 | import { vertexShader, fragmentShader } from './shader.glsl'; 5 | import { getRenderBufferSize } from '../../../resize'; 6 | import renderer from '../../../renderer'; 7 | import BaseScene from '../../../../scenes/base/base-scene'; 8 | import settings from '../../../../settings'; 9 | const animate = require('gsap-promisify')(Promise, TweenLite); 10 | 11 | /** 12 | * Transition pass handles transitioning between two scenes 13 | * 14 | * @export 15 | * @class TransitionPass 16 | */ 17 | export default class TransitionPass { 18 | gui: GUI; 19 | scene: Scene; 20 | camera: OrthographicCamera; 21 | active: boolean; 22 | mesh: Mesh; 23 | 24 | constructor(gui: GUI, geometry: BufferGeometry, camera: OrthographicCamera) { 25 | // Create gui 26 | this.gui = gui.addFolder('transition pass'); 27 | this.gui.open(); 28 | // Create scene 29 | this.scene = new Scene(); 30 | this.camera = camera; 31 | this.active = false; 32 | const { width, height } = getRenderBufferSize(); 33 | // Setup shader 34 | const material = new ShaderMaterial({ 35 | uniforms: { 36 | texture0: { 37 | value: null 38 | }, 39 | texture1: { 40 | value: null 41 | }, 42 | transition: { 43 | value: 0 44 | }, 45 | resolution: { 46 | value: new Vector2(width, height) 47 | } 48 | }, 49 | vertexShader, 50 | fragmentShader 51 | }); 52 | 53 | // Create the mesh and turn off matrixAutoUpdate 54 | this.mesh = new Mesh(geometry, material); 55 | this.mesh.matrixAutoUpdate = false; 56 | this.mesh.updateMatrix(); 57 | this.scene.add(this.mesh); 58 | 59 | // Setup gui 60 | this.gui 61 | .add(this.mesh.material.uniforms.transition, 'value', 0, 1) 62 | .onChange((value: number) => { 63 | this.active = value !== 0 && value !== 1; 64 | }) 65 | .name('transition') 66 | .listen(); 67 | } 68 | 69 | /** 70 | * Transition activates this pass and blends from sceneA to sceneB 71 | * 72 | * @memberof TransitionPass 73 | */ 74 | async transition() { 75 | if (settings.skipTransitions) { 76 | this.mesh.material.uniforms.transition.value = 1; 77 | } else { 78 | this.mesh.material.uniforms.transition.value = 0; 79 | this.active = true; 80 | TweenLite.killTweensOf(this.mesh.material.uniforms.transition); 81 | await animate 82 | .to(this.mesh.material.uniforms.transition, 1, { 83 | value: 1 84 | }) 85 | .then(() => { 86 | this.active = false; 87 | }); 88 | } 89 | } 90 | 91 | /** 92 | * Resize handler 93 | * 94 | * @param {Number} width 95 | * @param {Number} height 96 | * @memberof TransitionPass 97 | */ 98 | resize(width: number, height: number) { 99 | this.mesh.material.uniforms.resolution.value.x = width; 100 | this.mesh.material.uniforms.resolution.value.y = height; 101 | } 102 | 103 | /** 104 | * Render both scenes to renderTargetA and renderTargetB 105 | * 106 | * @param {BaseScene} sceneA 107 | * @param {BaseScene} sceneB 108 | * @param {WebGLRenderTarget} renderTargetA 109 | * @param {WebGLRenderTarget} renderTargetB 110 | * @param {Number} delta 111 | * @memberof TransitionPass 112 | */ 113 | render( 114 | sceneA: BaseScene, 115 | sceneB: BaseScene, 116 | renderTargetA: WebGLRenderTarget, 117 | renderTargetB: WebGLRenderTarget, 118 | delta: number 119 | ) { 120 | sceneA.update(delta); 121 | sceneB.update(delta); 122 | renderer.setClearColor(sceneA.clearColor); 123 | renderer.setRenderTarget(renderTargetA); 124 | renderer.render(sceneA.scene, sceneA.camera); 125 | renderer.setClearColor(sceneB.clearColor); 126 | renderer.setRenderTarget(renderTargetB); 127 | renderer.render(sceneB.scene, sceneB.camera); 128 | this.mesh.material.uniforms.texture0.value = renderTargetA.texture; 129 | this.mesh.material.uniforms.texture1.value = renderTargetB.texture; 130 | renderer.setRenderTarget(null); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/webgl-app/interaction/interactive-object.js: -------------------------------------------------------------------------------- 1 | import { Object3D, Raycaster, Vector2, PerspectiveCamera } from 'three'; 2 | import EventEmitter from 'eventemitter3'; 3 | import TouchControls from './touch-controls'; 4 | import type pointersArray from './touch-controls'; 5 | import renderer from '../rendering/renderer'; 6 | 7 | /** 8 | * Adds mouse and touch events to Object3D inherited objects 9 | * 10 | * @export 11 | * @class InteractiveObject 12 | * @extends {EventEmitter} 13 | */ 14 | export default class InteractiveObject extends EventEmitter { 15 | constructor(object: Object3D, camera: PerspectiveCamera, options: Object = {}) { 16 | super(); 17 | this.object = object; 18 | this.camera = camera; 19 | this.options = Object.assign( 20 | { 21 | mouseMove: false, // raycast everytime the mouse moves 22 | touchStart: true, // only fires when clicking down on an object successfully 23 | touchMove: true, // fires when mouse or touch is moved on and off an object 24 | touchEnd: true // fires when touch or mouse is released on and off an object 25 | }, 26 | options 27 | ); 28 | this.touchControls = new TouchControls(renderer.domElement, { hover: true }); 29 | this.raycaster = new Raycaster(); 30 | this.coords = new Vector2(); 31 | this.intersects = null; 32 | this.fired = { 33 | hoverOut: true, // Only fire hover out once per rollover 34 | hoverOver: false // Only fire hover out once per rollover 35 | }; 36 | this.bindEvents(true); 37 | } 38 | 39 | /** 40 | * Bind mouse and touch events 41 | * 42 | * @memberof InteractiveObject 43 | */ 44 | bindEvents = (bind: boolean) => { 45 | const listener = bind ? 'on' : 'off'; 46 | if (this.options.touchStart) this.touchControls[listener]('start', this.onTouchStart); 47 | if (this.options.touchMove) this.touchControls[listener]('move', this.onTouchMove); 48 | if (this.options.touchMove) this.touchControls[listener]('mousemove', this.onTouchMove); 49 | if (this.options.touchEnd || this.options.touchMove) this.touchControls[listener]('end', this.onTouchEnd); 50 | }; 51 | 52 | /** 53 | * Touch start handler 54 | * 55 | * @memberof InteractiveObject 56 | */ 57 | onTouchStart = (event: pointersArray[]) => { 58 | this.setCoords(event[0].normalX, event[0].normalY); 59 | this.intersected = this.raycast(); 60 | if (this.intersected) this.emit('start', this.intersects[0]); 61 | }; 62 | 63 | /** 64 | * Touch and mouse move handler 65 | * 66 | * @memberof InteractiveObject 67 | */ 68 | onTouchMove = (event: pointersArray[]) => { 69 | this.setCoords(event[0].normalX, event[0].normalY); 70 | this.intersected = this.raycast(); 71 | this.hovering = this.intersected; 72 | if (this.intersected) { 73 | if (!this.fired.hoverOver || this.options.mouseMove) this.emit('hover', true, this.intersects[0]); 74 | this.fired.hoverOut = false; 75 | this.fired.hoverOver = true; 76 | } else if (!this.fired.hoverOut) { 77 | this.fired.hoverOut = true; 78 | this.fired.hoverOver = false; 79 | this.emit('hover', false); 80 | } 81 | }; 82 | 83 | /** 84 | * Touch and hover out handler 85 | * 86 | * @memberof InteractiveObject 87 | */ 88 | onTouchEnd = (event: pointersArray[]) => { 89 | if (this.hovering) { 90 | this.hovering = false; 91 | this.emit('hover', false); 92 | } 93 | if (this.intersected) { 94 | this.intersected = false; 95 | this.emit('end'); 96 | } 97 | }; 98 | 99 | /** 100 | * Set the screenspace coords for the raycaster 101 | * 102 | * @memberof InteractiveObject 103 | */ 104 | setCoords = (normalX: number, normalY: number) => { 105 | this.coords.x = normalX * 2 - 1; 106 | this.coords.y = -normalY * 2 + 1; 107 | }; 108 | 109 | /** 110 | * Raycast against the object 111 | * 112 | * @memberof InteractiveObject 113 | */ 114 | raycast = (): boolean => { 115 | this.raycaster.setFromCamera(this.coords, this.camera); 116 | this.intersects = this.raycaster.intersectObject(this.object); 117 | return this.intersects.length > 0; 118 | }; 119 | 120 | /** 121 | * Dispose and unbind events 122 | * 123 | * @memberof InteractiveObject 124 | */ 125 | dispose = () => { 126 | this.touchControls.dispose(); 127 | this.bindEvents(false); 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/landing/objects/particles/particles.js: -------------------------------------------------------------------------------- 1 | import { GUI } from 'dat.gui'; 2 | import { 3 | BufferAttribute, 4 | BufferGeometry, 5 | ShaderMaterial, 6 | Vector3, 7 | Points, 8 | Math as Math3, 9 | Mesh, 10 | PerspectiveCamera, 11 | Scene, 12 | WebGLRenderTarget 13 | } from 'three'; 14 | import { vertexShader, fragmentShader } from './shader.glsl'; 15 | import ParticlesNormal from './particles-normal'; 16 | import { createRenderTarget } from '../../../../rendering/render-target'; 17 | import { getRenderBufferSize } from '../../../../rendering/resize'; 18 | import renderer from '../../../../rendering/renderer'; 19 | 20 | export default class Particles { 21 | config: Object; 22 | attributes: { 23 | [key: string]: BufferAttribute 24 | }; 25 | mesh: Mesh; 26 | renderTarget: WebGLRenderTarget; 27 | scene: Scene; 28 | gui: GUI; 29 | 30 | constructor(gui: GUI, totalParticles: number, particlesNormal: ParticlesNormal, pixelRatio: number) { 31 | this.gui = gui.addFolder('particles'); 32 | this.gui.open(); 33 | 34 | // Config to adjust particles 35 | this.config = { 36 | totalParticles, 37 | size: { 38 | min: 0.1, 39 | max: 5 40 | } 41 | }; 42 | 43 | // Create scene 44 | this.scene = new Scene(); 45 | 46 | const { width, height } = getRenderBufferSize(); 47 | this.renderTarget = createRenderTarget(width, height); 48 | 49 | // Create two attributes for positions and size 50 | this.attributes = { 51 | position: new BufferAttribute(new Float32Array(this.config.totalParticles * 3), 3), 52 | size: new BufferAttribute(new Float32Array(this.config.totalParticles), 1) 53 | }; 54 | 55 | // Set initial position and scale for particles 56 | for (let i = 0; i < this.config.totalParticles; i++) { 57 | const { x, y, z } = this.spherePoint(0, 0, 0, Math.random(), Math.random(), Math3.randFloat(10, 50)); 58 | this.attributes.position.setXYZ(i, x, y, z); 59 | 60 | const size = Math3.randFloat(this.config.size.min, this.config.size.max) * pixelRatio; 61 | this.attributes.size.setX(i, size); 62 | } 63 | 64 | // Setup buffer geometry 65 | const geometry = new BufferGeometry(); 66 | geometry.setAttribute('position', this.attributes.position); 67 | geometry.setAttribute('size', this.attributes.size); 68 | 69 | // Setup custom shader material 70 | const material = new ShaderMaterial({ 71 | uniforms: { 72 | particleSize: { value: 100 }, // Scale particles uniformly 73 | lightDirection: { value: new Vector3(1, 1, 1) }, // Light direction for lambert shading 74 | normalMap: { 75 | value: particlesNormal.renderTarget.texture // Normal map 76 | } 77 | }, 78 | vertexShader, 79 | fragmentShader 80 | }); 81 | 82 | // Add gui slider to tweak light direction 83 | this.gui.add(material.uniforms.lightDirection.value, 'x', -1, 1).name('light x'); 84 | this.gui.add(material.uniforms.lightDirection.value, 'y', -1, 1).name('light y'); 85 | this.gui.add(material.uniforms.lightDirection.value, 'z', -1, 1).name('light z'); 86 | 87 | // Create points mesh 88 | this.mesh = new Points(geometry, material); 89 | this.scene.add(this.mesh); 90 | } 91 | 92 | /** 93 | * Resize handler 94 | * 95 | * @memberof Particles 96 | */ 97 | resize() { 98 | const { width, height } = getRenderBufferSize(); 99 | this.renderTarget.setSize(width, height); 100 | } 101 | 102 | /** 103 | * Render the scene into the render target 104 | * 105 | * @param {number} delta 106 | * @param {PerspectiveCamera} camera 107 | * @memberof Particles 108 | */ 109 | render(delta: number, camera: PerspectiveCamera) { 110 | this.mesh.rotation.y += delta * 0.1; 111 | renderer.setRenderTarget(this.renderTarget); 112 | renderer.render(this.scene, camera); 113 | renderer.setRenderTarget(null); 114 | } 115 | 116 | /** 117 | * Util for random spherical distribution 118 | * 119 | * @param {number} x0 120 | * @param {number} y0 121 | * @param {number} z0 122 | * @param {number} u 123 | * @param {number} v 124 | * @param {number} radius 125 | * @returns 126 | * @memberof Particles 127 | */ 128 | spherePoint(x0: number, y0: number, z0: number, u: number, v: number, radius: number) { 129 | const theta = 2 * Math.PI * u; 130 | const phi = Math.acos(2 * v - 1); 131 | const x = x0 + radius * Math.sin(phi) * Math.cos(theta); 132 | const y = y0 + radius * Math.sin(phi) * Math.sin(theta); 133 | const z = z0 + radius * Math.cos(phi); 134 | return { x, y, z }; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/webgl-app/interaction/touch-controls.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import detect from '@jam3/detect'; 3 | 4 | type touchControlsOptions = {| 5 | touchStart?: boolean, 6 | touchMove?: boolean, 7 | touchEnd?: boolean, 8 | hover?: boolean 9 | |}; 10 | 11 | export type pointersArray = {| 12 | x: number, 13 | y: number, 14 | normalX: number, 15 | normalY: number 16 | |}; 17 | 18 | type pointersEvent = {| 19 | push: ({| x: number, y: number, normalX: number, normalY: number |}) => void, 20 | touches: {| 21 | pageX: boolean, 22 | pageY: boolean, 23 | length: number 24 | |}, 25 | MouseEvent: MouseEvent 26 | |}; 27 | 28 | /** 29 | * A class to normalize mouse and touch events 30 | * 31 | * @export 32 | * @class TouchControls 33 | * @extends {EventEmitter} 34 | */ 35 | export default class TouchControls extends EventEmitter { 36 | constructor(element: HTMLElement, options: touchControlsOptions) { 37 | super(); 38 | this.element = element; 39 | this.pointers = []; 40 | this.options = Object.assign( 41 | { 42 | hover: false, // mouse only 43 | touchStart: true, 44 | touchMove: true, 45 | touchEnd: true 46 | }, 47 | options 48 | ); 49 | this.touchesLength = 0; 50 | this.isDown = false; 51 | this.bindEvents(true); 52 | } 53 | 54 | /** 55 | * Bind mouse and touch events 56 | * 57 | * @memberof TouchControls 58 | */ 59 | bindEvents = (bind: boolean) => { 60 | const listener = bind ? 'addEventListener' : 'removeEventListener'; 61 | const isDesktop = detect.device.isDesktop; 62 | if (this.options.touchStart) this.element[listener](isDesktop ? 'mousedown' : 'touchstart', this.onTouchStart); 63 | if (this.options.touchMove) this.element[listener](isDesktop ? 'mousemove' : 'touchmove', this.onTouchMove); 64 | if (this.options.touchEnd) this.element[listener](isDesktop ? 'mouseup' : 'touchend', this.onTouchEnd); 65 | if (isDesktop) { 66 | if (this.options.hover) this.element[listener]('mouseover', this.onMouseOver); 67 | if (this.options.hover) this.element[listener]('mouseout', this.onMouseOut); 68 | } 69 | }; 70 | 71 | /** 72 | * Update the list of current inputs 73 | * and set the data 74 | * 75 | * @memberof TouchControls 76 | */ 77 | setPointers = (event: pointersEvent) => { 78 | this.pointers = []; 79 | if (event.touches) { 80 | this.touchesLength = event.touches.length; 81 | for (let i = 0; i < this.touchesLength; i++) { 82 | const pointer = event.touches[i]; 83 | this.pointers.push({ 84 | x: pointer.pageX, 85 | y: pointer.pageY, 86 | normalX: pointer.pageX / window.innerWidth, 87 | normalY: pointer.pageY / window.innerHeight 88 | }); 89 | } 90 | } else { 91 | this.pointers.push({ 92 | x: event.pageX, 93 | y: event.pageY, 94 | normalX: event.pageX / window.innerWidth, 95 | normalY: event.pageY / window.innerHeight 96 | }); 97 | } 98 | }; 99 | 100 | /** 101 | * Touch start handler 102 | * 103 | * @memberof TouchControls 104 | */ 105 | onTouchStart = (event: pointersEvent) => { 106 | this.isDown = true; 107 | this.setPointers(event); 108 | this.emit('start', this.pointers); 109 | }; 110 | 111 | /** 112 | * Touch move handler 113 | * 114 | * @memberof TouchControls 115 | */ 116 | onTouchMove = (event: pointersEvent) => { 117 | this.onMouseMove(event); 118 | if (!this.isDown) return; 119 | this.setPointers(event); 120 | this.emit('move', this.pointers); 121 | }; 122 | 123 | /** 124 | * Touch end handler 125 | * 126 | * @memberof TouchControls 127 | */ 128 | onTouchEnd = () => { 129 | this.isDown = false; 130 | this.emit('end', this.pointers); 131 | }; 132 | 133 | /** 134 | * Mouse move handler 135 | * 136 | * @memberof TouchControls 137 | */ 138 | onMouseMove = (event: pointersEvent) => { 139 | this.setPointers(event); 140 | this.emit('mousemove', this.pointers); 141 | }; 142 | 143 | /** 144 | * Mouse over handler 145 | * 146 | * @memberof TouchControls 147 | */ 148 | onMouseOver = (event: pointersEvent) => { 149 | this.emit('hover', true); 150 | }; 151 | 152 | /** 153 | * Mouse out handler 154 | * 155 | * @memberof TouchControls 156 | */ 157 | onMouseOut = (event: pointersEvent) => { 158 | this.emit('hover', false); 159 | }; 160 | 161 | /** 162 | * Dispose and unbind events 163 | * 164 | * @memberof TouchControls 165 | */ 166 | dispose = () => { 167 | this.bindEvents(false); 168 | }; 169 | } 170 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/landing/objects/particles/particles-normal.js: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | Mesh, 4 | SphereBufferGeometry, 5 | ShaderMaterial, 6 | PerspectiveCamera, 7 | RGBAFormat, 8 | WebGLRenderTarget, 9 | WebGLRenderer 10 | } from 'three'; 11 | import { VECTOR_ZERO } from '../../../../utils/math'; 12 | import createCanvas from '../../../../utils/canvas'; 13 | 14 | // Render target size 15 | const TEXTURE_SIZE = 128; 16 | // Preview render target in canvas for debugging 17 | const DEBUG_CANVAS = false; 18 | 19 | export default class ParticlesNormal { 20 | renderer: WebGLRenderer; 21 | scene: Scene; 22 | camera: PerspectiveCamera; 23 | renderTarget: WebGLRenderTarget; 24 | mesh: Mesh; 25 | canvas: HTMLCanvasElement; 26 | ctx: CanvasRenderingContext2D; 27 | canvasFlipped: HTMLCanvasElement; 28 | ctxFlipped: CanvasRenderingContext2D; 29 | pixelBuffer: Uint8Array; 30 | imageData: ImageData; 31 | 32 | constructor(renderer: WebGLRenderer) { 33 | this.renderer = renderer; 34 | // Create an empty scene 35 | this.scene = new Scene(); 36 | // Create a new perspective camera 37 | this.camera = new PerspectiveCamera(60, 1, 0.01, 5); 38 | // Camera position is set the diameter of the sphere away 39 | this.camera.position.set(0, 0, 2); 40 | // Look at the center 41 | this.camera.lookAt(VECTOR_ZERO); 42 | // Create render target texture for normal map 43 | this.renderTarget = new WebGLRenderTarget(TEXTURE_SIZE, TEXTURE_SIZE, { 44 | format: RGBAFormat, 45 | stencilBuffer: false 46 | }); 47 | 48 | // Setup sphere mesh 49 | this.mesh = new Mesh( 50 | new SphereBufferGeometry(1, 32, 32), 51 | new ShaderMaterial({ 52 | vertexShader: ` 53 | varying vec3 vNormal; 54 | void main() { 55 | vNormal = normal; 56 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 57 | } 58 | `, 59 | fragmentShader: ` 60 | varying vec3 vNormal; 61 | void main() { 62 | // Pack the normal range from (-1, 1), to (0, 1) 63 | gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0); 64 | } 65 | ` 66 | }) 67 | ); 68 | this.scene.add(this.mesh); 69 | 70 | // Create debug canvases to preview the render target output 71 | // Note: Render target outputs pixels on the y-axis inverted 72 | if (DEBUG_CANVAS) { 73 | const { canvas, ctx } = createCanvas(TEXTURE_SIZE, TEXTURE_SIZE); 74 | const { canvas: canvasFlipped, ctx: ctxFlipped } = createCanvas(TEXTURE_SIZE, TEXTURE_SIZE); 75 | this.canvas = canvas; 76 | this.ctx = ctx; 77 | this.canvasFlipped = canvasFlipped; 78 | this.ctxFlipped = ctxFlipped; 79 | 80 | this.pixelBuffer = new Uint8Array(this.renderTarget.width * this.renderTarget.height * 4); 81 | this.imageData = this.ctxFlipped.createImageData(this.canvas.width, this.canvas.height); 82 | 83 | Object.assign(canvas.style, { 84 | top: '0px', 85 | left: '80px', 86 | position: 'absolute', 87 | zIndex: '1000', 88 | pointerEvents: 'none', 89 | width: `${TEXTURE_SIZE / 2}px`, 90 | height: `${TEXTURE_SIZE / 2}px` 91 | }); 92 | 93 | Object.assign(canvasFlipped.style, { 94 | top: '0px', 95 | left: `${80 + TEXTURE_SIZE / 2}px`, 96 | position: 'absolute', 97 | zIndex: '1000', 98 | pointerEvents: 'none', 99 | width: `${TEXTURE_SIZE / 2}px`, 100 | height: `${TEXTURE_SIZE / 2}px` 101 | }); 102 | 103 | if (document.body) document.body.appendChild(canvas); 104 | if (document.body) document.body.appendChild(canvasFlipped); 105 | } 106 | } 107 | 108 | /** 109 | * Render the scene into the render target 110 | * 111 | * @param {PerspectiveCamera} camera 112 | * @memberof ParticlesNormal 113 | */ 114 | render(camera: PerspectiveCamera) { 115 | // Set the active render target 116 | this.renderer.setRenderTarget(this.renderTarget); 117 | // Copy the camera position but limit the length 118 | this.camera.position.copy(camera.position).setLength(2); 119 | // Ensure the camera is looking at the center 120 | this.camera.lookAt(VECTOR_ZERO); 121 | // Render the scene 122 | this.renderer.render(this.scene, this.camera); 123 | 124 | if (DEBUG_CANVAS) { 125 | // Output the render target pixels into the pixel buffer 126 | this.renderer.readRenderTargetPixels( 127 | this.renderTarget, 128 | 0, 129 | 0, 130 | this.renderTarget.width, 131 | this.renderTarget.height, 132 | this.pixelBuffer 133 | ); 134 | // Update the image data 135 | this.imageData.data.set(this.pixelBuffer); 136 | this.ctxFlipped.putImageData(this.imageData, 0, 0); 137 | this.ctx.save(); 138 | // Flip the canvas on the y-axis 139 | this.ctx.scale(1, -1); 140 | // Draw the image the correct way 141 | this.ctx.drawImage(this.canvasFlipped, 0, -this.canvas.height, this.canvas.width, this.canvas.height); 142 | this.ctx.restore(); 143 | } 144 | // Reset the render target 145 | this.renderer.setRenderTarget(null); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/webgl-app/utils/screenshot.js: -------------------------------------------------------------------------------- 1 | import { WebGLRenderTarget, LinearFilter, RGBAFormat, PerspectiveCamera } from 'three'; 2 | import { GUI } from 'dat.gui'; 3 | import { saveAs } from 'file-saver'; 4 | import createCanvas from './canvas'; 5 | import { rendererSize } from '../rendering/resize'; 6 | import renderer, { postProcessing } from '../rendering/renderer'; 7 | import BaseScene from '../scenes/base/base-scene'; 8 | 9 | const DEBUG_RENDER = false; 10 | 11 | /** 12 | * This screenshot utility renders out a custom size render and saves it to an image 13 | * Please note if the post processing passes change it will require updating 14 | * 15 | * @export 16 | * @class Screenshot 17 | */ 18 | export default class Screenshot { 19 | gui: GUI; 20 | renderTargetA: WebGLRenderTarget; 21 | renderTargetB: WebGLRenderTarget; 22 | imageData: ImageData; 23 | canvas: HTMLCanvasElement; 24 | canvasFlipped: HTMLCanvasElement; 25 | ctx: CanvasRenderingContext2D; 26 | ctxFlipped: CanvasRenderingContext2D; 27 | width: number; 28 | height: number; 29 | pixelBuffer: Uint8Array; 30 | 31 | constructor(gui: GUI, width: number, height: number, pixelRatio: number = 1) { 32 | this.gui = gui.addFolder('screenshot'); 33 | this.gui.open(); 34 | this.width = width * pixelRatio; 35 | this.height = height * pixelRatio; 36 | 37 | this.renderTargetA = new WebGLRenderTarget(this.width, this.height, { 38 | minFilter: LinearFilter, 39 | magFilter: LinearFilter, 40 | format: RGBAFormat, 41 | stencilBuffer: false 42 | }); 43 | this.renderTargetB = new WebGLRenderTarget(this.width, this.height, { 44 | minFilter: LinearFilter, 45 | magFilter: LinearFilter, 46 | format: RGBAFormat, 47 | stencilBuffer: false 48 | }); 49 | 50 | const { canvas, ctx } = createCanvas(this.width, this.height); 51 | const { canvas: canvasFlipped, ctx: ctxFlipped } = createCanvas(this.width, this.height); 52 | 53 | this.canvas = canvas; 54 | this.canvasFlipped = canvasFlipped; 55 | this.ctx = ctx; 56 | this.ctxFlipped = ctxFlipped; 57 | 58 | this.pixelBuffer = new Uint8Array(this.renderTargetA.width * this.renderTargetA.height * 4); 59 | this.imageData = this.ctxFlipped.createImageData(this.canvas.width, this.canvas.height); 60 | 61 | if (DEBUG_RENDER) { 62 | Object.assign(this.canvas.style, { 63 | position: 'absolute', 64 | top: '0', 65 | left: '0', 66 | zIndex: '100', 67 | border: '1px solid white', 68 | pointerEvents: 'none', 69 | width: `${width}px`, 70 | height: `${height}px` 71 | }); 72 | if (document.body) document.body.appendChild(this.canvas); 73 | } 74 | } 75 | 76 | /** 77 | * Save the canvas to an image 78 | * 79 | * @memberof Screenshot 80 | */ 81 | save = () => { 82 | const quality = 0.75; 83 | const filename = 'screenshot.jpg'; 84 | const format = 'image/jpeg'; 85 | this.canvas.toBlob( 86 | function(blob) { 87 | saveAs(blob, filename); 88 | }, 89 | format, 90 | quality 91 | ); 92 | }; 93 | 94 | /** 95 | * Capture the current scene and save to an image 96 | * 97 | * @memberof Screenshot 98 | */ 99 | capture = (scene: BaseScene, camera: PerspectiveCamera) => { 100 | // Clear current context 101 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 102 | 103 | // Save aspect for resetting after render 104 | const aspect = camera.aspect; 105 | 106 | // Update aspect to the screenshot size ratio 107 | camera.aspect = this.width / this.height; 108 | camera.updateProjectionMatrix(); 109 | 110 | // Save current width / height 111 | const finalPassWidth = postProcessing.finalPass.mesh.material.uniforms.resolution.value.x; 112 | const finalPassHeight = postProcessing.finalPass.mesh.material.uniforms.resolution.value.y; 113 | const left = 0; 114 | const bottom = 0; 115 | const width = rendererSize.x; 116 | const height = rendererSize.y; 117 | 118 | // Update renderer viewport, this will get reset in the main render loop 119 | // inside webgl-app.js 120 | renderer.setViewport(left, bottom, width, height); 121 | renderer.setScissor(left, bottom, width, height); 122 | 123 | // Update the final pass uniforms 124 | postProcessing.finalPass.resize(this.width, this.height); 125 | 126 | // Render the current scene into renderTargetA 127 | renderer.setRenderTarget(this.renderTargetA); 128 | renderer.render(scene, camera); 129 | renderer.setRenderTarget(null); 130 | 131 | // Apply the post processing fx which is output into renderTargetB 132 | postProcessing.finalPass.screenshotRender(scene, camera, this.renderTargetA, this.renderTargetB, 0); 133 | // Put the rendered pixels into the pixelBuffer 134 | renderer.readRenderTargetPixels( 135 | this.renderTargetB, 136 | 0, 137 | 0, 138 | this.renderTargetB.width, 139 | this.renderTargetB.height, 140 | this.pixelBuffer 141 | ); 142 | this.imageData.data.set(this.pixelBuffer); 143 | 144 | // The image is rendered upside down, so we flip it 145 | this.ctxFlipped.putImageData(this.imageData, 0, 0); 146 | this.ctx.save(); 147 | this.ctx.scale(1, -1); 148 | this.ctx.drawImage(this.canvasFlipped, 0, -this.canvas.height, this.canvas.width, this.canvas.height); 149 | this.ctx.restore(); 150 | 151 | // Reset the camera aspect 152 | camera.aspect = aspect; 153 | camera.updateProjectionMatrix(); 154 | 155 | // Reset the finalpass uniforms 156 | postProcessing.finalPass.resize(finalPassWidth, finalPassHeight); 157 | 158 | // Save out the image 159 | this.save(); 160 | }; 161 | } 162 | -------------------------------------------------------------------------------- /src/webgl-app/cameras/camera-dolly/camera-dolly-manager.js: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera, Group, Vector3 } from 'three'; 2 | import { TweenMax, Power1 } from 'gsap/gsap-core'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 4 | import { GUI } from 'dat.gui'; 5 | import Dolly from './camera-dolly'; 6 | import type DollyData from './camera-dolly'; 7 | import type HelperOptions from './camera-dolly'; 8 | import { GUIWrapper } from '../../utils/gui'; 9 | 10 | export type CameraDollyManagerOptions = {| 11 | gui: GUI, 12 | guiOpen: boolean 13 | |}; 14 | 15 | export type DollyOptions = {| 16 | gui: GUI, 17 | guiOpen?: boolean 18 | |}; 19 | 20 | export default class CameraDollyManager { 21 | dollies: { 22 | [key: string]: Dolly 23 | }; 24 | time: number; 25 | gui: GUI; 26 | tracksGui: GUI; 27 | group: Group; 28 | dollyId: string; 29 | dollyIds: string[]; 30 | lookat: Vector3; 31 | camera: PerspectiveCamera; 32 | options: CameraDollyManagerOptions; 33 | 34 | constructor(options: CameraDollyManagerOptions) { 35 | // Current playback time 0 - 1 36 | this.time = 0; 37 | 38 | // Container for any 3d objects 39 | this.group = new Group(); 40 | 41 | // Dollies added 42 | this.dollies = {}; 43 | 44 | // Active dolly id 45 | this.dollyId = ''; 46 | 47 | // Array of dolly track ids 48 | this.dollyIds = []; 49 | 50 | // Lookat vector 51 | this.lookat = new Vector3(); 52 | 53 | // Active camera 54 | this.camera = null; 55 | 56 | this.options = options; 57 | 58 | // Create GUI instance 59 | if (options.gui) { 60 | this.gui = options.gui.addFolder('camera dolly manager'); 61 | if (options.guiOpen) this.gui.open(); 62 | 63 | this.gui 64 | .add(this, 'time', 0, 1) 65 | .listen() 66 | .onChange(this.update); 67 | } else { 68 | this.gui = new GUIWrapper(); 69 | } 70 | 71 | // Add tracks GUI 72 | // Since the list can change if more dollies are added 73 | // We recreate this gui everytime addTransition is called 74 | this.tracksGui = this.gui.addFolder('tracks'); 75 | this.tracksGui.open(); 76 | } 77 | 78 | /** 79 | * Add a transition 80 | * Note two camereas are required for the transform controls to work 81 | * 82 | * @param {string} id 83 | * @param {DollyData} data 84 | * @param {PerspectiveCamera} cameraMain 85 | * @param {PerspectiveCamera} cameraDev 86 | * @param {OrbitControls} control 87 | * @memberof CameraDollyManager 88 | */ 89 | addTransition( 90 | id: string, 91 | data: DollyData, 92 | cameraMain: PerspectiveCamera, 93 | cameraDev: PerspectiveCamera, 94 | control: OrbitControls, 95 | helperOptions: HelperOptions 96 | ) { 97 | this.dollies[id] = new Dolly(id, data, this.gui, cameraDev, control, helperOptions); 98 | this.dollies[id].on('rebuild', this.update); 99 | this.group.add(this.dollies[id].group); 100 | this.setTransition(id, cameraMain); 101 | } 102 | 103 | /** 104 | * Set the current transition 105 | * 106 | * @param {string} id 107 | * @param {PerspectiveCamera} camera 108 | * @memberof CameraDollyManager 109 | */ 110 | setTransition(id: string, camera: PerspectiveCamera) { 111 | // Set the new dolly id 112 | this.dollyId = id; 113 | 114 | // Add the id to the dolly list 115 | if (!this.dollyIds.includes(id)) this.dollyIds.push(id); 116 | 117 | // Remove and recreate the tracks gui 118 | this.gui.removeFolder(this.tracksGui.name); 119 | this.tracksGui = this.gui.addFolder('tracks'); 120 | this.tracksGui.open(); 121 | this.tracksGui.add(this, 'dollyId', this.dollyIds).onChange(this.onTrackChange); 122 | 123 | // Set the active camera 124 | this.camera = camera; 125 | 126 | // Show the active dolly path, if helpers are visible 127 | Object.keys(this.dollies).forEach((key: string) => { 128 | const visible = key === id; 129 | this.dollies[key].toggleVisibility(visible); 130 | }); 131 | 132 | // Update camera 133 | this.update(); 134 | } 135 | 136 | /** 137 | * Switch to the new track 138 | * 139 | * @memberof CameraDollyManager 140 | */ 141 | onTrackChange = (value: string) => { 142 | this.setTransition(value, this.camera); 143 | }; 144 | 145 | /** 146 | * Animate the current track 147 | * 148 | * @param {number} [duration=1] 149 | * @param {Object} [ease=Power1.easeOut] 150 | * @memberof CameraDollyManager 151 | */ 152 | async transition(duration: number = 1, ease: Object = Power1.easeOut) { 153 | await new Promise((resolve, reject) => { 154 | TweenMax.killTweensOf(this); 155 | this.time = 0; 156 | this.update(); 157 | TweenMax.to(this, duration, { 158 | time: 1, 159 | ease, 160 | onUpdate: () => { 161 | this.update(); 162 | }, 163 | onComplete: () => { 164 | resolve(); 165 | } 166 | }); 167 | }); 168 | } 169 | 170 | /** 171 | * Stop current playback 172 | * 173 | * @memberof CameraDollyManager 174 | */ 175 | stop() { 176 | TweenMax.killTweensOf(this); 177 | } 178 | 179 | /** 180 | * Update camera position and playback 181 | * 182 | * @memberof CameraDollyManager 183 | */ 184 | update = () => { 185 | if (this.dollies[this.dollyId] === undefined) return; 186 | const { origin, lookat } = this.dollies[this.dollyId].getCameraDataByTime(this.time); 187 | this.camera.position.set(origin.x, origin.y, origin.z); 188 | this.lookat.set(lookat.x, lookat.y, lookat.z); 189 | this.camera.lookAt(this.lookat); 190 | }; 191 | 192 | /** 193 | * Dispose 194 | * 195 | * @memberof CameraDollyManager 196 | */ 197 | dispose() { 198 | this.stop(); 199 | Object.keys(this.dollies).forEach((id: string) => { 200 | this.dollies[id].dispose(); 201 | }); 202 | if (this.options.gui) { 203 | this.options.gui.removeFolder(this.gui.name); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGL React App 2 | 3 | ![](https://img.shields.io/david/dev/jam3/webgl-react-app) 4 | ![](https://img.shields.io/github/license/jam3/webgl-react-app) 5 | 6 | ![WebGL React App](webgl-react-app.gif) 7 | 8 | The goal is this project is to standardise WebGL and React based projects at Jam3. 9 | 10 | Building upon experience it features highly optimised approaches for rendering and scene management. 11 | 12 | This is a great starting place for creative coders who want to jump straight into coding webgl. 13 | 14 | ## Features 15 | 16 | **Flow** 17 | 18 | This Project uses [Flow](https://flow.org/) typing. A great place to start is with the [docs](https://flow.org/en/docs/) or this [cheatsheet](https://devhints.io/flow). 19 | 20 | **Rendering** 21 | 22 | - [Graphics profiling](src/webgl-app/rendering/profiler.js) 23 | - Preload objects on [GPU](src/webgl-app/rendering/preload-gpu.js) 24 | - Post Processing 25 | - [FXAA](src/webgl-app/rendering/post-processing/passes/fxaa.glsl.js) as a replacement for antialising when using PostProcessing on WebGL 1 26 | - [Film Pass](src/webgl-app/rendering/post-processing/passes/film.glsl.js) for a more filmic look 27 | - [Transition Pass](src/webgl-app/rendering/post-processing/passes/transition-pass/transition-pass.js) for blending between two webgl scenes 28 | - [Final Pass](src/webgl-app/rendering/post-processing/passes/final-pass/final-pass.js) Combine multiple effects in a single shader 29 | - [Stats](src/webgl-app/utils/stats.js) for fps and threejs for performance insights 30 | 31 | **Scenes** 32 | 33 | - [BaseScene](src/webgl-app/scenes/base/base-scene.js), an extendable class that enforces a clean scene pattern 34 | - [EventEmitter3](https://github.com/primus/eventemitter3) is used for event communication between classes 35 | 36 | **Cameras** 37 | 38 | - Helpers for [creating perspective cameras](src/webgl-app/cameras/cameras.js#L30) and adding [orbit controls](src/webgl-app/cameras/cameras.js#L41) 39 | 40 | **Lights** 41 | 42 | - Helpers added for [Ambient Light](src/webgl-app/lights/ambient.js), [Directional Light](src/webgl-app/lights/directional.js), [Point Light](src/webgl-app/lights/point.js) and [Spot Light](src/webgl-app/lights/spot.js) 43 | 44 | **Materials** 45 | 46 | - A [material modifier](src/webgl-app/utils/material-modifier.js) inspired by [three-material-modifier](https://github.com/jamieowen/three-material-modifier) that can extend three's built in Materials with custom shader code 47 | 48 | **Interactions** 49 | 50 | - [Touch Controls](src/webgl-app/interaction/touch-controls.js) for normalizing mouse and touch events 51 | - [InteractiveObject](src/webgl-app/interaction/interactive-object.js) adds interactivity to meshes 52 | 53 | **Asset Optimsing** 54 | 55 | - [TextureOptimiser](scripts/assets/texture-optimiser.js) for compressing and resizing webgl textures 56 | - [ModelOptimiser](scripts/assets/model-optimiser.js) for converting fbx models to gltf with draco compression 57 | 58 | **Asset Management** 59 | 60 | - [AssetLoader](src/webgl-app/loading/asset-loader.js) for loading an array of assets with different types 61 | - [AssetManager](src/webgl-app/loading/asset-manager.js) for storing and retriving assets loaded with the AssetLoader 62 | 63 | ## Precommit and Husky 64 | 65 | Sometimes husky doesn't run if you're using Git software. 66 | 67 | To check this, open the console output in your Git software and make sure the pre-commit hook isn't bypassed. 68 | 69 | If husky isn't working create a `~/.huskyrc` file and add: 70 | 71 | ``` 72 | # ~/.huskyrc 73 | export NVM_DIR="$HOME/.nvm" 74 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 75 | ``` 76 | 77 | ## References 78 | 79 | - [Threejs documentation](https://threejs.org/docs/) 80 | - [Discover threejs Tips and Tricks](https://discoverthreejs.com/tips-and-tricks/) 81 | 82 | ## Contributing 83 | 84 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting 85 | pull requests. 86 | 87 | ## License 88 | 89 | [MIT](LICENSE) 90 | 91 | --- 92 | 93 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 94 | 95 | ## Available Scripts 96 | 97 | In the project directory, you can run: 98 | 99 | ### `npm start` 100 | 101 | Runs the app in the development mode.
102 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 103 | 104 | The page will reload if you make edits.
105 | You will also see any lint errors in the console. 106 | 107 | ### `npm test` 108 | 109 | Launches the test runner in the interactive watch mode.
110 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 111 | 112 | ### `npm build` 113 | 114 | Builds the app for production to the `build` folder.
115 | It correctly bundles React in production mode and optimizes the build for the best performance. 116 | 117 | The build is minified and the filenames include the hashes.
118 | Your app is ready to be deployed! 119 | 120 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 121 | 122 | ### `npm eject` 123 | 124 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 125 | 126 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 127 | 128 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 129 | 130 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 131 | 132 | ## Learn More 133 | 134 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 135 | 136 | To learn React, check out the [React documentation](https://reactjs.org/). 137 | 138 | ### Code Splitting 139 | 140 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 141 | 142 | ### Analyzing the Bundle Size 143 | 144 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 145 | 146 | ### Making a Progressive Web App 147 | 148 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 149 | 150 | ### Advanced Configuration 151 | 152 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 153 | 154 | ### Deployment 155 | 156 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 157 | 158 | ### `npm build` fails to minify 159 | 160 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 161 | -------------------------------------------------------------------------------- /src/webgl-app/scenes/base/base-scene.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import EventEmitter from 'eventemitter3'; 4 | import { Scene, Group, GridHelper, AxesHelper } from 'three'; 5 | import { createPerspectiveCamera, createOrbitControls, resetCamera } from '../../cameras/cameras'; 6 | import { gui, GUIWrapper } from '../../utils/gui'; 7 | import Math3 from '../../utils/math'; 8 | import settings from '../../settings'; 9 | import { rendererSize } from '../../rendering/resize'; 10 | import preloadGpu from '../../rendering/preload-gpu'; 11 | import assetLoader from '../../loading/asset-loader'; 12 | import assetManager from '../../loading/asset-manager'; 13 | import Asset from '../../loading/asset'; 14 | import disposeObjects from '../../utils/dispose-objects'; 15 | 16 | /** 17 | * A base scene for other scenes to inherit 18 | * It's main purpose is to abtract a lot of boilerplate code and serves 19 | * as a pattern for working with multiple scenes in a project 20 | * 21 | * @export 22 | * @class BaseScene 23 | * @extends {EventEmitter} 24 | */ 25 | export default class BaseScene extends EventEmitter { 26 | constructor(options: Object) { 27 | super(); 28 | // Unique scene id 29 | this.id = options.id || Math3.generateUUID(); 30 | // Clear color for the scene 31 | this.clearColor = options.clearColor || 0x000000; 32 | // Array of lights to add to the scene 33 | this.lights = options.lights || []; 34 | // Assets manifest 35 | this.assets = options.assets || []; 36 | // The scene for objects 37 | this.scene = new Scene(); 38 | 39 | // The cameras for rendering 40 | this.cameras = { 41 | dev: createPerspectiveCamera(rendererSize.x / rendererSize.y), 42 | main: createPerspectiveCamera(rendererSize.x / rendererSize.y) 43 | }; 44 | 45 | // Active rendering camera 46 | this.camera = settings.devCamera ? this.cameras.dev : this.cameras.main; 47 | 48 | // Set the initial camera positions 49 | resetCamera(this.cameras.dev, 5); 50 | resetCamera(this.cameras.main, 5); 51 | 52 | // Orbit controls 53 | this.controls = {}; 54 | 55 | // Optionally create orbit controls for main camera 56 | if (options.controls) { 57 | this.controls.dev = createOrbitControls(this.cameras.dev); 58 | this.controls.main = createOrbitControls(this.cameras.main); 59 | } 60 | 61 | // Active camera control 62 | this.control = settings.devCamera ? this.controls.dev : this.controls.main; 63 | 64 | // Optionally create gui controls 65 | if (options.gui) { 66 | this.gui = gui.addFolder(`${this.id} scene`); 67 | if (options.guiOpen) this.gui.open(); 68 | } else { 69 | this.gui = new GUIWrapper(); 70 | } 71 | 72 | // Add any lights to the scene 73 | this.lights.forEach(light => { 74 | this.scene.add(light.light); 75 | light.gui(this.gui); 76 | }); 77 | } 78 | 79 | /** 80 | * 81 | * 82 | * @memberof BaseScene 83 | */ 84 | async loadAssets() { 85 | await new Promise((resolve, reject) => { 86 | try { 87 | if (this.assets.length > 0) { 88 | assetLoader.once('loaded', (response: Asset[]) => { 89 | if (response.length > 0) assetManager.add(this.id, response); 90 | resolve(); 91 | }); 92 | assetLoader.once('error', error => { 93 | reject(error); 94 | }); 95 | assetLoader.load(this.id, this.assets); 96 | } else { 97 | resolve(); 98 | } 99 | } catch (error) { 100 | reject(error); 101 | } 102 | }); 103 | } 104 | 105 | /** 106 | * Use this function to setup any helpers for the scene 107 | * 108 | * @memberof BaseScene 109 | */ 110 | async createSceneHelpers() { 111 | await new Promise((resolve: Function, reject: Function) => { 112 | try { 113 | // Add helpers 114 | this.helpers = new Group(); 115 | this.helpers.add(new GridHelper(10, 10), new AxesHelper()); 116 | this.helpers.visible = settings.helpers; 117 | this.scene.add(this.helpers); 118 | resolve(); 119 | } catch (error) { 120 | reject(error); 121 | } 122 | }); 123 | } 124 | 125 | /** 126 | * Use this function to setup any 3d objects once overridden 127 | * 128 | * @memberof BaseScene 129 | */ 130 | async createSceneObjects() { 131 | await new Promise((resolve, reject) => { 132 | try { 133 | resolve(); 134 | } catch (error) { 135 | reject(error); 136 | } 137 | }); 138 | } 139 | 140 | /** 141 | * Use this function to show any materials or objects that can't be seen 142 | * using the visible flag inside preloadGpu 143 | * An example of this could be a materials alpha is set to 0 144 | * 145 | * @memberof BaseScene 146 | */ 147 | preloadGpuCullScene = (culled: boolean) => {}; 148 | 149 | /** 150 | * Setup is used to create any 3D objects 151 | * and pre-upload them to the GPU to ensure smooth transitions when rendering 152 | * 153 | * @memberof BaseScene 154 | */ 155 | async setup() { 156 | await this.loadAssets(); 157 | await this.createSceneHelpers(); 158 | await this.createSceneObjects(); 159 | this.preloadGpuCullScene(true); 160 | preloadGpu(this.scene, this.camera); 161 | this.preloadGpuCullScene(false); 162 | } 163 | 164 | /** 165 | * Toggle helpers on and off 166 | * 167 | * @memberof BaseScene 168 | */ 169 | toggleHelpers = (visible: boolean = true) => { 170 | this.helpers.visible = visible; 171 | }; 172 | 173 | /** 174 | * Toggle helpers on and off 175 | * 176 | * @memberof BaseScene 177 | */ 178 | toogleCameras = (devCamera: boolean = true) => { 179 | this.camera = devCamera ? this.cameras.dev : this.cameras.main; 180 | this.control = devCamera ? this.controls.dev : this.controls.main; 181 | }; 182 | 183 | /** 184 | * Resize the camera's projection matrix 185 | * 186 | * @memberof BaseScene 187 | */ 188 | resize = (width: number, height: number) => { 189 | this.cameras.dev.aspect = width / height; 190 | this.cameras.dev.updateProjectionMatrix(); 191 | this.cameras.main.aspect = width / height; 192 | this.cameras.main.updateProjectionMatrix(); 193 | }; 194 | 195 | /** 196 | * Provide a promise after the scene has animated in 197 | * 198 | * @memberof BaseScene 199 | */ 200 | async animateIn() { 201 | await new Promise((resolve, reject) => { 202 | try { 203 | resolve(); 204 | } catch (error) { 205 | reject(error); 206 | } 207 | }); 208 | } 209 | 210 | /** 211 | * Provide a promise after the scene has animated out 212 | * 213 | * @memberof BaseScene 214 | */ 215 | async animateOut() { 216 | await new Promise((resolve, reject) => { 217 | try { 218 | resolve(); 219 | } catch (error) { 220 | reject(error); 221 | } 222 | }); 223 | } 224 | 225 | /** 226 | * Update loop for animation, override this function 227 | * 228 | * @memberof BaseScene 229 | */ 230 | update = (delta: number) => {}; 231 | 232 | /** 233 | * Clear up scene objects 234 | * 235 | * @memberof BaseScene 236 | */ 237 | dispose = () => { 238 | disposeObjects(this.scene, null); 239 | if (this.gui) gui.removeFolder(this.gui.name); 240 | }; 241 | } 242 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | > A code of conduct is a set of rules outlining the social norms and rules and responsibilities of, or proper practices 4 | > for, an individual, party or organization 5 | 6 | ## Summary 7 | 8 | This code of conduct is dedicated to providing a harassment-free working environment for all, regardless of gender, sexual orientation, disability, physical appearance, body size, race, or religion. We do not tolerate harassment of any form. All communication should be appropriate for a professional audience including people of many different backgrounds. 9 | 10 | Sexual language and imagery are not appropriate for any communication or talks. Be kind and do not insult or put down others. Behave professionally. Remember that harassment and sexist, racist, or exclusionary jokes are not appropriate for this project. Staff violating these rules should be reported to an appropriate line manager. 11 | 12 | These are the values to which people in the project should aspire: 13 | 14 | - Be friendly and welcoming 15 | - Be patient 16 | - Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) 17 | - Be thoughtful 18 | - Productive communication requires effort. Think about how your words will be interpreted. 19 | - Remember that sometimes it is best to refrain entirely from commenting. 20 | - Be respectful 21 | - In particular, respect differences of opinion. 22 | - Be charitable 23 | - Interpret the arguments of others in good faith, do not seek to disagree. 24 | - When we do disagree, try to understand why. 25 | - Avoid destructive behaviour: 26 | - Derailing: stay on topic; if you want to talk about something else, start a new conversation. 27 | - Unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved. 28 | - Snarking (pithy, unproductive, sniping comments) 29 | - Discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict. 30 | - Microaggressions: brief and commonplace verbal, behavioural and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group. 31 | 32 | People are complicated. You should expect to be misunderstood and to misunderstand others; when this inevitably occurs, resist the urge to be defensive or assign blame. Try not to take offence where no offence was intended. Give people the benefit of the doubt. Even if the intent was to provoke, do not rise to it. It is the responsibility of all parties to de-escalate conflict when it arises. 33 | 34 | ## Reporting an incident 35 | 36 | Incidents that violate the Code of Conduct are extremely damaging to the project, and they will not be tolerated. The silver lining is that, in many cases, these incidents present a chance for the offenders, and the teams at large, to grow, learn, and become better. 37 | 38 | > The following should be handled by a line manager who has been informed of the incident 39 | 40 | Try to get as much of the incident in written form. The important information to gather include the following: 41 | 42 | - Name and the team of the participant doing the harassing 43 | - The location in which the incident occurred 44 | - The behaviour that was in violation 45 | - The approximate time of the behaviour 46 | - The circumstances surrounding the incident 47 | - Other people involved in the incident 48 | 49 | Depending on the severity/details of the incident, please follow these guidelines: 50 | 51 | - If there is any general threat to staff or any other doubts, summon security or police 52 | - Offer the victim a private place to sit 53 | - Ask "is there a friend or trusted person whom you would like to be with you?" (if so, arrange for someone to fetch this person) 54 | - Ask them "how can I help?" 55 | - Provide them with your list of emergency contacts if they need help later 56 | - If everyone is presently physically safe, involve the police or security only at a victim's request 57 | 58 | There are also some guidelines as to what not to do as an initial response: 59 | 60 | - Do not overtly invite them to withdraw the complaint or mention that withdrawal is OK. This suggests that you want them to do so, and is therefore coercive. "If you're OK with pursuing the complaint" suggests that you are by default pursuing it and is not coercive. 61 | - Do not ask for their advice on how to deal with the complaint. This is a staff responsibility. 62 | - Do not offer them input into penalties. This is the staff's responsibility. 63 | 64 | The line manager who is handling the reported offence should find out the following: 65 | 66 | - What happened? 67 | - Are we doing anything about it? 68 | - Who is doing those things? 69 | - When are they doing them? 70 | 71 | After the above has been identified and discussed, have an appropriate line manager communicate with the alleged harasser. Make sure to inform them of what has been reported about them. 72 | 73 | Allow the alleged harasser to give their side of the story. After this point, if the report stands, let the alleged harasser know what actions will be taken against them. 74 | 75 | Some things for the staff to consider when dealing with Code of Conduct offenders: 76 | 77 | - Warning the harasser to cease their behaviour and that any further reports will result in sanctions 78 | - Requiring that the harasser avoid any interaction with, and physical proximity to, their victim until a resolution or course of action has been decided upon 79 | - Requiring that the harasser not volunteer for future events your organization runs (either indefinitely or for a certain period) 80 | - Depending on the severity/details of the incident, requiring that the harasser immediately be sent home 81 | - Depending on the severity/details of the incident, removing a harasser from membership of relevant projects 82 | - Depending on the severity/details of the incident, publishing an account of the harassment and calling for the resignation of the harasser from their responsibilities (usually pursued by people without formal authority: may be called for if the harasser is a team leader, or refuses to stand aside from the conflict of interest) 83 | 84 | Give accused staff members a place to appeal to if there is one, but in the meantime, the report stands. Keep in mind that it is not a good idea to encourage an apology from the harasser. 85 | 86 | It is essential how we deal with the incident publicly. Our policy is to make sure that everyone aware of the initial incident is also made aware that it is not according to policy and that official action has been taken - while still respecting the privacy of individual staff members. When speaking to individuals (those who are aware of the incident, but were not involved with the incident) about the incident, it is a good idea to keep the details out. 87 | 88 | Depending on the incident, the head of the responsible department, or designate, may decide to make one or more public announcements. If necessary, this will be done with a short announcement either during the plenary and/or through other channels. No one other than the head of the responsible department or someone delegated authority from them should make any announcements. No personal information about either party will be disclosed as part of this process. 89 | 90 | If some members of staff were angered by the incident, it is best to apologize to them that the incident occurred, to begin with. If there are residual hard feelings, suggest to them to write an email to the responsible head of the department. It will be dealt with accordingly. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct was adapted from both [Golang](https://golang.org/conduct) a the [Golang UK Conference](http://golanguk.com/conduct/). 95 | -------------------------------------------------------------------------------- /src/webgl-app/webgl-app.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import { Clock, Vector4, PerspectiveCamera } from 'three'; 3 | import renderer, { postProcessing } from './rendering/renderer'; 4 | import { setRendererSize, rendererSize } from './rendering/resize'; 5 | import settings from './settings'; 6 | import { rendererStats } from './utils/stats'; 7 | import { setQuery, getQueryFromParams } from './utils/query-params'; 8 | import { gui } from './utils/gui'; 9 | import PreloaderScene, { PRELOADER_SCENE_ID } from './scenes/preloader/preloader-scene'; 10 | import AppState from './app-state'; 11 | import LandingScene, { LANDING_SCENE_ID } from './scenes/landing/landing-scene'; 12 | import CameraTransitionScene, { 13 | CAMERA_TRANSITION_SCENE_ID 14 | } from './scenes/camera-transitions/camera-transitions-scene'; 15 | import Screenshot from './utils/screenshot'; 16 | import InteractiveSphereScene, { 17 | INTERACTIVE_SPHERE_SCENE_ID 18 | } from './scenes/interactive-sphere/interactive-sphere-scene'; 19 | 20 | class WebGLApp extends EventEmitter { 21 | /** 22 | * Creates an instance of WebGLApp. 23 | * @param {HTMLElement} parent 24 | * @memberof WebGLApp 25 | */ 26 | constructor(parent: HTMLElement) { 27 | super(); 28 | // Append the renderer canvas to the component reference 29 | parent.appendChild(renderer.domElement); 30 | 31 | // Clock for elapsed time and delta 32 | this.clock = new Clock(true); 33 | 34 | // Current request animation frame id 35 | this.rafId = 0; 36 | 37 | // Current frame delta 38 | this.delta = 0; 39 | 40 | // Flag to prevent multiple raf's running 41 | this.isRendering = false; 42 | 43 | // Initial state 44 | this.state = new AppState({ ready: false }); 45 | 46 | // Scenes map 47 | this.scenes = { 48 | [PRELOADER_SCENE_ID]: PreloaderScene, 49 | [LANDING_SCENE_ID]: LandingScene, 50 | [INTERACTIVE_SPHERE_SCENE_ID]: InteractiveSphereScene, 51 | [CAMERA_TRANSITION_SCENE_ID]: CameraTransitionScene 52 | }; 53 | // List of ids to switch between 54 | const sceneIds = [LANDING_SCENE_ID, INTERACTIVE_SPHERE_SCENE_ID, CAMERA_TRANSITION_SCENE_ID]; 55 | 56 | // The target scene id 57 | this.sceneId = LANDING_SCENE_ID; 58 | if (sceneIds.includes(getQueryFromParams('sceneId'))) { 59 | this.sceneId = getQueryFromParams('sceneId'); 60 | } 61 | 62 | this.viewport = { 63 | debug: new Vector4( 64 | 0, 65 | 0, 66 | rendererSize.x * settings.viewportPreviewScale, 67 | rendererSize.y * settings.viewportPreviewScale 68 | ), 69 | main: new Vector4(0, 0, rendererSize.x, rendererSize.y) 70 | }; 71 | 72 | // Add screenshot utility 73 | this.screenshot = new Screenshot(gui, 1280, 720, 2); 74 | this.screenshot.gui.add(this, 'captureScreenshot').name('capture'); 75 | 76 | // Gui settings group 77 | const guiSettings = gui.addFolder('settings'); 78 | guiSettings.open(); 79 | 80 | // Toggle between dev and scene camera 81 | guiSettings.add(settings, 'devCamera').onChange((value: string) => { 82 | setQuery('devCamera', value); 83 | postProcessing.resize(); 84 | this.currentScene.toogleCameras(value); 85 | }); 86 | 87 | // Toggle scene helpers 88 | guiSettings.add(settings, 'helpers').onChange((value: string) => { 89 | setQuery('helpers', value); 90 | this.currentScene.toggleHelpers(value); 91 | }); 92 | 93 | // Toggle between scenes 94 | guiSettings 95 | .add(this, 'sceneId', sceneIds) 96 | .onChange((value: string) => { 97 | this.setScene(value); 98 | setQuery('sceneId', value); 99 | }) 100 | .listen(); 101 | } 102 | 103 | captureScreenshot = () => { 104 | this.screenshot.capture(this.currentScene.scene, this.currentScene.camera); 105 | }; 106 | 107 | /** 108 | * Setup any 109 | * 110 | * @memberof WebGLApp 111 | */ 112 | async setup() { 113 | await new Promise((resolve, reject) => { 114 | try { 115 | // Setup the preloader scene right away as we need a scene to render on page load 116 | this.setScene(PRELOADER_SCENE_ID) 117 | .then(resolve) 118 | .catch(reject); 119 | } catch (error) { 120 | reject(error); 121 | } 122 | }); 123 | } 124 | 125 | // Set the new state 126 | setState = (state: AppState) => { 127 | if (state.equals(this.state)) return; 128 | this.prevState = this.state.clone(); 129 | this.state = state; 130 | this.onStateChanged(this.state); 131 | }; 132 | 133 | onStateChanged = (state: AppState) => { 134 | if (this.state.ready && this.state.ready !== this.prevState.ready) { 135 | this.setScene(this.sceneId); 136 | } 137 | }; 138 | 139 | /** 140 | * Set the current scene to render 141 | * The scene should be inheritted from BaseScene 142 | * 143 | * @param {BaseScene} scene 144 | * @memberof WebGLApp 145 | */ 146 | async setScene(sceneId: string) { 147 | await new Promise((resolve, reject) => { 148 | if (this.currentScene && sceneId === this.currentScene.id) return; 149 | // Create new scene instance 150 | const scene = new this.scenes[sceneId](); 151 | scene 152 | .setup() 153 | .then(() => { 154 | // Cache the previous scene 155 | const previousScene = this.currentScene; 156 | // Callback when the previous scene has animated out 157 | const nextScene = () => { 158 | // Set the current scene 159 | this.currentScene = scene; 160 | // Animate the scene in 161 | this.currentScene.animateIn().then(resolve, reject); 162 | // Update the post processing scene transition pass 163 | postProcessing.setScenes(postProcessing.sceneB, scene); 164 | postProcessing.transitionPass.transition().then(() => { 165 | // After the transition has ended, dispose of any objects 166 | if (previousScene) previousScene.dispose(); 167 | }); 168 | }; 169 | // If the previous scene exists, animate out 170 | if (previousScene) { 171 | previousScene 172 | .animateOut() 173 | .then(nextScene) 174 | .catch(reject); 175 | } else { 176 | // Otherwise go to the next scene immediately 177 | nextScene(); 178 | } 179 | }) 180 | .catch(reject); 181 | }); 182 | } 183 | 184 | /** 185 | * resize handler 186 | * 187 | * @memberof WebGLApp 188 | */ 189 | resize = (width: number, height: number) => { 190 | setRendererSize(renderer, width, height); 191 | this.currentScene.resize(width, height); 192 | postProcessing.resize(); 193 | this.viewport.debug.set( 194 | 0, 195 | 0, 196 | rendererSize.x * settings.viewportPreviewScale, 197 | rendererSize.y * settings.viewportPreviewScale 198 | ); 199 | this.viewport.main.set(0, 0, rendererSize.x, rendererSize.y); 200 | }; 201 | 202 | /** 203 | * Render the scene within viewport coordinates 204 | * 205 | * @memberof WebGLApp 206 | */ 207 | renderScene = (camera: PerspectiveCamera, viewport: Vector4, delta: number, usePostProcessing: boolean) => { 208 | renderer.setViewport(viewport.x, viewport.y, viewport.z, viewport.w); 209 | renderer.setScissor(viewport.x, viewport.y, viewport.z, viewport.w); 210 | 211 | if (usePostProcessing) { 212 | postProcessing.render(delta); 213 | } else { 214 | this.currentScene.update(this.delta); 215 | renderer.setClearColor(this.currentScene.clearColor); 216 | renderer.render(this.currentScene.scene, camera); 217 | } 218 | }; 219 | 220 | /** 221 | * Toggle the rendering and animation loop 222 | * 223 | * @memberof WebGLApp 224 | */ 225 | render = (render: boolean) => { 226 | if (this.isRendering === render) return; 227 | this.isRendering = render; 228 | if (render) { 229 | this.update(); 230 | } else { 231 | cancelAnimationFrame(this.rafId); 232 | } 233 | }; 234 | 235 | /** 236 | * Main render loop and update of animations 237 | * 238 | * @memberof WebGLApp 239 | */ 240 | update = () => { 241 | this.rafId = requestAnimationFrame(this.update); 242 | this.delta = this.clock.getDelta(); 243 | 244 | if (settings.devCamera) { 245 | this.renderScene(this.currentScene.cameras.dev, this.viewport.main, this.delta, false); 246 | this.renderScene(this.currentScene.cameras.main, this.viewport.debug, this.delta, true); 247 | } else { 248 | this.renderScene(this.currentScene.cameras.main, this.viewport.main, this.delta, true); 249 | } 250 | 251 | if (settings.stats) { 252 | rendererStats.update(renderer); 253 | } 254 | }; 255 | } 256 | 257 | export default WebGLApp; 258 | -------------------------------------------------------------------------------- /src/webgl-app/cameras/camera-dolly/camera-dolly.js: -------------------------------------------------------------------------------- 1 | import { 2 | Vector3, 3 | CatmullRomCurve3, 4 | PerspectiveCamera, 5 | Group, 6 | Mesh, 7 | SphereBufferGeometry, 8 | MeshBasicMaterial, 9 | Geometry, 10 | Line, 11 | LineBasicMaterial 12 | } from 'three'; 13 | import EventEmitter from 'eventemitter3'; 14 | import { GUI } from 'dat.gui'; 15 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 16 | import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; 17 | import renderer from '../../rendering/renderer'; 18 | import { GUIWrapper } from '../../utils/gui'; 19 | 20 | export type DollyPoint = {| 21 | x: number, 22 | y: number, 23 | z: number 24 | |}; 25 | 26 | export interface DollyData { 27 | steps: number; 28 | origin: DollyPoint[]; 29 | lookat: DollyPoint[]; 30 | } 31 | 32 | export type HelperOptions = { 33 | linesVisible: boolean, 34 | controlsVisible: boolean, 35 | pointsVisible: boolean 36 | }; 37 | 38 | const ORIGIN = 'origin'; 39 | const LOOKAT = 'lookat'; 40 | 41 | // Create reuseable geometry and materials 42 | const helperGeometry = new SphereBufferGeometry(0.1, 16, 16); 43 | const helperMaterial = new MeshBasicMaterial(); 44 | const helperLineMaterialOrigin = new LineBasicMaterial({ 45 | color: 0xffffff 46 | }); 47 | const helperLineMaterialLookat = new LineBasicMaterial({ 48 | color: 0xffff00 49 | }); 50 | 51 | export default class CameraDolly extends EventEmitter { 52 | steps: number; 53 | origin: Vector3[]; 54 | lookat: Vector3[]; 55 | curves: Object; 56 | 57 | constructor( 58 | id: string, 59 | data: DollyData, 60 | gui: GUI | GUIWrapper, 61 | camera: PerspectiveCamera, 62 | control: OrbitControls, 63 | helperOptions: HelperOptions 64 | ) { 65 | super(); 66 | // Container to contain any 3d objects 67 | this.group = new Group(); 68 | // Smoothness of camera path 69 | this.steps = data.steps; 70 | // The dev camera 71 | this.camera = camera; 72 | // Active orbit control 73 | this.control = control; 74 | // Origin points 75 | this.origin = []; 76 | // Lookat points 77 | this.lookat = []; 78 | // Create gui 79 | this.gui = gui.addFolder(`${id} camera dolly`); 80 | 81 | // Convert point to vectors 82 | data.origin.forEach((point: DollyPoint) => { 83 | this.origin.push(new Vector3(point.x, point.y, point.z)); 84 | }); 85 | 86 | data.lookat.forEach((point: DollyPoint) => { 87 | this.lookat.push(new Vector3(point.x, point.y, point.z)); 88 | }); 89 | 90 | // Create curves 91 | this.curves = { 92 | [ORIGIN]: this.createSmoothSpline(this.origin, this.steps), 93 | [LOOKAT]: this.createSmoothSpline(this.lookat, this.steps) 94 | }; 95 | 96 | // Add transform controls 97 | this.controls = new Group(); 98 | this.controls.visible = helperOptions.controlsVisible; 99 | this.group.add(this.controls); 100 | 101 | this.points = new Group(); 102 | this.points.visible = helperOptions.pointsVisible; 103 | this.group.add(this.points); 104 | 105 | // List of positions from each path 106 | // These get updated from the transform controls 107 | this.curvePoints = { 108 | [ORIGIN]: [], 109 | [LOOKAT]: [] 110 | }; 111 | 112 | // Add a transform control for each point 113 | this.origin.forEach((point: Vector3, i: number) => { 114 | this.addControl(ORIGIN, i, point); 115 | }); 116 | this.lookat.forEach((point: Vector3, i: number) => { 117 | this.addControl(LOOKAT, i, point); 118 | }); 119 | 120 | // Create visible curves 121 | this.lines = new Group(); 122 | this.lines.visible = helperOptions.linesVisible; 123 | // List of currently visible line meshes 124 | this.lineMeshes = []; 125 | this.group.add(this.lines); 126 | 127 | // Create helper lines to see the paths 128 | this.createLine(this.curves.origin.points, helperLineMaterialOrigin); 129 | this.createLine(this.curves.lookat.points, helperLineMaterialLookat); 130 | 131 | this.gui.add(this, 'steps', 5, 100, 1).onChange(this.rebuild); 132 | this.gui 133 | .add(this.controls, 'visible') 134 | .name('controls') 135 | .onChange((value: boolean) => { 136 | this.toggleControls(value); 137 | }); 138 | this.gui.add(this.points, 'visible').name('points'); 139 | this.gui.add(this.lines, 'visible').name('lines'); 140 | this.gui.add(this, 'export'); 141 | this.gui.open(); 142 | } 143 | 144 | /** 145 | * Toggle the visibility of the helpers and gui 146 | * 147 | * @memberof CameraDolly 148 | */ 149 | toggleVisibility = (visible: boolean) => { 150 | this.group.visible = visible; 151 | this.gui[visible ? 'open' : 'close'](); 152 | this.toggleControls(visible); 153 | }; 154 | 155 | /** 156 | * Toggle transform controls 157 | * 158 | * @param {boolean} enabled 159 | * @memberof CameraDolly 160 | */ 161 | toggleControls(enabled: boolean) { 162 | for (let i = 0; i < this.controls.children.length; i++) { 163 | this.controls.children[i].enabled = this.controls.visible && this.group.visible; 164 | } 165 | } 166 | 167 | /** 168 | * Create a smooth spline from the data points 169 | * 170 | * @memberof CameraDolly 171 | */ 172 | createSmoothSpline = (positions: Vector3[], totalPoints: number = 10) => { 173 | let curve = new CatmullRomCurve3(positions); 174 | const points = curve.getPoints(totalPoints); 175 | curve = new CatmullRomCurve3(points); 176 | return { 177 | curve, 178 | points 179 | }; 180 | }; 181 | 182 | /** 183 | * Get the camera origin and lookat by a nornalised time value 0 - 1 184 | * 185 | * @memberof Dolly 186 | */ 187 | getCameraDataByTime = (time: number = 0) => { 188 | const origin: Vector3 = this.curves.origin.curve.getPointAt(time); 189 | const lookat: Vector3 = this.curves.lookat.curve.getPointAt(time); 190 | return { 191 | origin, 192 | lookat 193 | }; 194 | }; 195 | 196 | /** 197 | * Recreate the curves after the points change 198 | * 199 | * @memberof CameraDolly 200 | */ 201 | updateSplines = () => { 202 | this.curves.origin = this.createSmoothSpline(this.origin, this.steps); 203 | this.curves.lookat = this.createSmoothSpline(this.lookat, this.steps); 204 | }; 205 | 206 | /** 207 | * Add a transform control and helper 208 | * 209 | * @memberof CameraDolly 210 | */ 211 | addControl = (id: string, index: number, point: Vector3) => { 212 | // Create mesh 213 | const mesh = new Mesh(helperGeometry, helperMaterial); 214 | mesh.position.copy(point); 215 | this.points.add(mesh); 216 | 217 | // Create control 218 | const control = new TransformControls(this.camera, renderer.domElement); 219 | control.enabled = this.controls.visible; 220 | this.controls.add(control); 221 | control.addEventListener('dragging-changed', this.onTransformChanged); 222 | control.attach(mesh); 223 | 224 | this.curvePoints[id][index] = mesh.position; 225 | }; 226 | 227 | /** 228 | * Create a helper line for the curve 229 | * 230 | * @memberof CameraDolly 231 | */ 232 | createLine = (vertices: Vector3[], material: LineBasicMaterial) => { 233 | const geometry = new Geometry(); 234 | geometry.vertices = vertices; 235 | const line = new Line(geometry, material); 236 | this.lines.add(line); 237 | this.lineMeshes.push(line); 238 | }; 239 | 240 | /** 241 | * Remove old lines 242 | * 243 | * @memberof CameraDolly 244 | */ 245 | removeLines() { 246 | for (let i = 0; i < this.lineMeshes.length; i++) { 247 | this.lines.remove(this.lineMeshes[i]); 248 | } 249 | } 250 | 251 | /** 252 | * When the transform control is manipulated, disable the orbit controls 253 | * 254 | * @memberof CameraDolly 255 | */ 256 | onTransformChanged = (event: any) => { 257 | this.control.enabled = !event.value; 258 | this.rebuild(); 259 | }; 260 | 261 | /** 262 | * Rebuild the curves and update the points 263 | * 264 | * @memberof CameraDolly 265 | */ 266 | rebuild = () => { 267 | for (let i = 0; i < this.origin.length; i++) { 268 | this.origin[i].copy(this.curvePoints[ORIGIN][i]); 269 | } 270 | for (let i = 0; i < this.lookat.length; i++) { 271 | this.lookat[i].copy(this.curvePoints[LOOKAT][i]); 272 | } 273 | this.updateSplines(); 274 | this.removeLines(); 275 | this.createLine(this.curves.origin.points, helperLineMaterialOrigin); 276 | this.createLine(this.curves.lookat.points, helperLineMaterialLookat); 277 | }; 278 | 279 | /** 280 | * Export data to json 281 | * 282 | * @memberof CameraDolly 283 | */ 284 | export = () => { 285 | const data = JSON.stringify( 286 | { 287 | steps: this.steps, 288 | origin: this.origin, 289 | lookat: this.lookat 290 | }, 291 | undefined, 292 | 2 293 | ); 294 | window.prompt('Copy to clipboard: Ctrl+C, Enter', data); 295 | }; 296 | 297 | /** 298 | * Dispose 299 | * 300 | * @memberof CameraDolly 301 | */ 302 | dispose = () => { 303 | for (let i = 0; i < this.controls.children.length; i++) { 304 | this.controls.children[i].removeEventListener('dragging-changed', this.onTransformDragChanged); 305 | } 306 | }; 307 | } 308 | --------------------------------------------------------------------------------