{
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 | 
4 | 
5 |
6 | 
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 |
--------------------------------------------------------------------------------