├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── LICENSE.md ├── README.md ├── build ├── sketches.json └── style.css ├── package-lock.json ├── package.json ├── screenshot.gif ├── src ├── js │ ├── 1-environment │ │ └── index.js │ ├── 2-particles │ │ ├── index.js │ │ └── particles │ │ │ ├── particles-normal.js │ │ │ ├── particles.js │ │ │ └── shader.glsl.js │ ├── 3-context-resizing │ │ └── index.js │ ├── 4-gpu-profiling │ │ ├── index.js │ │ └── profiler.js │ ├── 5-post-processing │ │ ├── index.js │ │ ├── post-processing.js │ │ └── shader.glsl.js │ ├── gui.js │ ├── render-stats.js │ ├── stats.js │ └── utils.js └── templates │ ├── 1-environment.pug │ ├── 2-particles.pug │ ├── 3-context-resizing.pug │ ├── 4-gpu-profiling.pug │ ├── 5-post-processing.pug │ ├── includes │ ├── _layout.pug │ ├── _partials.pug │ └── _sketch.pug │ └── index.pug └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs. 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | # We recommend you to keep these unchanged. 9 | indent_style = space 10 | indent_size = 2 11 | tab_width = 2 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [package.json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.glsl.js 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["airbnb-base", "prettier"], 4 | "env": { 5 | "browser": true, 6 | "es6": true 7 | }, 8 | "parser": "babel-eslint", 9 | "parserOptions": { 10 | "ecmaVersion": 6 11 | }, 12 | "rules": { 13 | "arrow-body-style": ["error", "as-needed"], 14 | "import/prefer-default-export": 0, 15 | "prefer-destructuring": 0, 16 | "import/no-named-as-default": 0, 17 | "comma-dangle": ["error", "never"], 18 | "no-param-reassign": 0, 19 | "no-bitwise": 0, 20 | "no-plusplus": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | .eslintcache 5 | *.js.map 6 | lib 7 | 8 | /build/js/*.js 9 | /build/*.html 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jam3 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Creative Coding Meetup 2 | 3 | A WebGL experiment created for Jam3's Creative Coding Meetup on October 23rd 2019. 4 | 5 | ![alt text](screenshot.gif 'WebGL experiment') 6 | 7 | ## Contents 8 | 9 | This experiment is divided into 5 parts, each demo builds upon the code of the previous. 10 | 11 | The aim is to demonstrate some of the best practices and workflows we use on WebGL experiences at Jam3. 12 | 13 | 1. [Environment](src/js/1-environment) 14 | 2. [Particles](src/js/2-particles) 15 | 3. [Context resizing](src/js/3-context-resizing) 16 | 4. [GPU Profiling](src/js/4-gpu-profiling) 17 | 5. [Post Processing](src/js/5-post-processing) 18 | 19 | ## Usage 20 | 21 | 1. `npm install` 22 | 2. `npm start` 23 | 24 | ## License 25 | 26 | MIT [License.md](LICENSE.md) 27 | -------------------------------------------------------------------------------- /build/sketches.json: -------------------------------------------------------------------------------- 1 | { 2 | "sketches": { 3 | "Steps": [ 4 | { 5 | "id": "1-environment", 6 | "title": "1. Setting up the environment" 7 | }, 8 | { 9 | "id": "2-particles", 10 | "title": "2. Adding some particles!" 11 | }, 12 | { 13 | "id": "3-context-resizing", 14 | "title": "3. Context resizing" 15 | }, 16 | { 17 | "id": "4-gpu-profiling", 18 | "title": "4. Profiling the gpu" 19 | }, 20 | { 21 | "id": "5-post-processing", 22 | "title": "5. Post Processing" 23 | } 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /build/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=PT+Serif'); 2 | 3 | html { 4 | font-size: 15px; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | font-family: 'PT Serif', serif; 10 | background-color: black; 11 | color: white; 12 | font-size: 1rem; 13 | -webkit-font-smoothing: antialiased; 14 | overflow: hidden; 15 | } 16 | 17 | h1 { 18 | font-size: 2.5rem; 19 | letter-spacing: 0.025em; 20 | margin: 0; 21 | } 22 | 23 | h3 { 24 | letter-spacing: 0.03em; 25 | } 26 | 27 | a { 28 | color: white; 29 | text-decoration: none; 30 | } 31 | 32 | a:hover { 33 | text-decoration: underline; 34 | } 35 | 36 | header { 37 | padding: 2rem; 38 | } 39 | 40 | .description { 41 | color: #bbbbbb; 42 | } 43 | 44 | .nav { 45 | display: flex; 46 | flex-direction: row; 47 | flex-wrap: wrap; 48 | } 49 | 50 | .nav__item { 51 | flex: 0 auto; 52 | display: flex; 53 | flex-direction: column; 54 | min-width: 10rem; 55 | min-height: 5rem; 56 | } 57 | 58 | .nav__item ul { 59 | margin: 0; 60 | padding: 0; 61 | list-style: none; 62 | } 63 | 64 | .nav__item ul li { 65 | margin: 0.5rem 0; 66 | } 67 | 68 | #fps { 69 | background-color: black !important; 70 | } 71 | 72 | #fpsGraph > span { 73 | background-color: black !important; 74 | } 75 | 76 | #fpsText { 77 | color: white !important; 78 | } 79 | 80 | #fpsGraph { 81 | background-color: white !important; 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "creative-coding-meetup", 3 | "version": "0.0.1", 4 | "description": "A WebGL experiment created for Jam3's Creative Coding Meetup on October 23rd 2019.", 5 | "scripts": { 6 | "start": "concurrently 'npm run server' 'npm run src:js' 'npm run src:html'", 7 | "server": "live-server ./build --port 3000 --quiet --watch ./build --watch ./build", 8 | "src:js": "webpack --colors --watch", 9 | "src:html": "npm run src:files; pug --silent --obj ./build/sketches.json --watch ./src/templates/*.pug --out ./build", 10 | "src:files": "pug --silent --obj ./build/sketches.json ./src/templates/*.pug --out ./build", 11 | "js-lint": "eslint './*.js' -c ./.eslintrc.json --quiet", 12 | "linters": "npm run js-lint" 13 | }, 14 | "husky": { 15 | "hooks": { 16 | "pre-commit": "echo 'Pre-commit checks...' && lint-staged", 17 | "pre-push": "echo 'Pre-push checks...' && npm run linters" 18 | } 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/Jam3/meetup-creative-coding-webgl.git" 23 | }, 24 | "author": "Amelie Rosser (https://www.ixviii.io)", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/Jam3/meetup-creative-coding-webgl/issues" 28 | }, 29 | "homepage": "https://github.com/Jam3/meetup-creative-coding-webgl#readme", 30 | "devDependencies": { 31 | "babel-cli": "^6.6.5", 32 | "babel-eslint": "^8.0.1", 33 | "babel-loader": "^7.1.2", 34 | "babel-preset-es2015": "^6.6.0", 35 | "babel-preset-stage-0": "^6.16.0", 36 | "concurrently": "^3.6.1", 37 | "eslint": "^4.10.0", 38 | "eslint-config-airbnb-base": "^12.1.0", 39 | "eslint-config-prettier": "^2.7.0", 40 | "eslint-plugin-import": "^2.8.0", 41 | "husky": "^3.0.9", 42 | "lint-staged": "^9.4.2", 43 | "live-server": "^1.2.0", 44 | "pug-cli": "^1.0.0-alpha6", 45 | "webpack": "^3.8.1" 46 | }, 47 | "dependencies": { 48 | "@jam3/stats": "^1.0.1", 49 | "dat.gui": "^0.7.6", 50 | "detect-gpu": "^1.1.2", 51 | "query-string": "^5.0.1", 52 | "three": "^0.109.0" 53 | }, 54 | "engines": { 55 | "node": ">=12.13.0", 56 | "npm": ">=6.12.0" 57 | }, 58 | "lint-staged": { 59 | "*.js": [ 60 | "eslint --fix", 61 | "prettier --write", 62 | "git add" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/meetup-creative-coding-webgl/0c07eb6505c464ed6dd14b9b38a6564424be7b4a/screenshot.gif -------------------------------------------------------------------------------- /src/js/1-environment/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | WebGLRenderer, 3 | PerspectiveCamera, 4 | Scene, 5 | GridHelper, 6 | AxesHelper, 7 | Vector3, 8 | CameraHelper 9 | } from 'three'; 10 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 11 | import { renderStats } from '../stats'; 12 | import { guiController } from '../gui'; 13 | 14 | // Setup the webgl renderer 15 | const renderer = new WebGLRenderer({ antialias: true }); 16 | renderer.debug.checkShaderErrors = true; 17 | 18 | renderer.setScissorTest(true); 19 | renderer.setPixelRatio(window.devicePixelRatio); 20 | renderer.setSize(window.innerWidth, window.innerHeight); 21 | document.body.appendChild(renderer.domElement); 22 | 23 | // Create two cameras 24 | // One for developing, the other for the final view 25 | const cameras = { 26 | dev: new PerspectiveCamera( 27 | 65, 28 | window.innerWidth / window.innerHeight, 29 | 0.1, 30 | 1000 31 | ), 32 | main: new PerspectiveCamera( 33 | 65, 34 | window.innerWidth / window.innerHeight, 35 | 0.1, 36 | 1000 37 | ) 38 | }; 39 | 40 | // Set initial camera positions 41 | cameras.dev.position.set(20, 10, 20); 42 | cameras.dev.lookAt(new Vector3()); 43 | 44 | cameras.main.position.set(0, 10, 20); 45 | cameras.main.lookAt(new Vector3()); 46 | 47 | // Create two sets of orbit controls 48 | // One for developing, the other for user control 49 | const controls = { 50 | dev: new OrbitControls(cameras.dev, renderer.domElement), 51 | main: new OrbitControls(cameras.main, renderer.domElement) 52 | }; 53 | controls.main.enableDamping = true; 54 | 55 | // Create our scene graph 56 | const scene = new Scene(); 57 | 58 | // Add some debug helpers 59 | scene.add( 60 | new GridHelper(10, 10), 61 | new AxesHelper(), 62 | new CameraHelper(cameras.main) 63 | ); 64 | 65 | // Render the scene with viewport coords 66 | function renderScene(camera, left, bottom, width, height) { 67 | left *= window.innerWidth; 68 | bottom *= window.innerHeight; 69 | width *= window.innerWidth; 70 | height *= window.innerHeight; 71 | 72 | renderer.setViewport(left, bottom, width, height); 73 | renderer.setScissor(left, bottom, width, height); 74 | 75 | renderer.render(scene, camera); 76 | } 77 | 78 | function update() { 79 | requestAnimationFrame(update); 80 | // Enable main camera controls when not in dev mode 81 | controls.main.enabled = !guiController.cameraDebug; 82 | controls.main.update(); 83 | 84 | // Handle scene rendering 85 | if (guiController.cameraDebug) { 86 | renderScene(cameras.dev, 0, 0, 1, 1); 87 | renderScene(cameras.main, 0, 0, 0.25, 0.25); 88 | } else { 89 | renderScene(cameras.main, 0, 0, 1, 1); 90 | } 91 | 92 | // Update render stats 93 | renderStats.update(renderer); 94 | } 95 | 96 | function onResize() { 97 | // Update camera projections 98 | cameras.dev.aspect = window.innerWidth / window.innerHeight; 99 | cameras.dev.updateProjectionMatrix(); 100 | // Set webgl context size 101 | renderer.setSize(window.innerWidth, window.innerHeight); 102 | } 103 | 104 | window.addEventListener('resize', onResize); 105 | 106 | // Begin render loop 107 | update(); 108 | -------------------------------------------------------------------------------- /src/js/2-particles/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | WebGLRenderer, 3 | PerspectiveCamera, 4 | Scene, 5 | GridHelper, 6 | AxesHelper, 7 | Vector3, 8 | CameraHelper, 9 | Group 10 | } from 'three'; 11 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 12 | import { renderStats } from '../stats'; 13 | import Particles from './particles/particles'; 14 | import ParticlesNormal from './particles/particles-normal'; 15 | import { guiController } from '../gui'; 16 | 17 | const renderer = new WebGLRenderer({ antialias: true }); 18 | renderer.debug.checkShaderErrors = true; 19 | 20 | renderer.setScissorTest(true); 21 | renderer.setPixelRatio(window.devicePixelRatio); 22 | renderer.setSize(window.innerWidth, window.innerHeight); 23 | document.body.appendChild(renderer.domElement); 24 | 25 | const cameras = { 26 | dev: new PerspectiveCamera( 27 | 65, 28 | window.innerWidth / window.innerHeight, 29 | 0.1, 30 | 1000 31 | ), 32 | main: new PerspectiveCamera( 33 | 65, 34 | window.innerWidth / window.innerHeight, 35 | 0.1, 36 | 1000 37 | ) 38 | }; 39 | 40 | cameras.dev.position.set(20, 10, 20); 41 | cameras.dev.lookAt(new Vector3()); 42 | 43 | cameras.main.position.set(0, 10, 20); 44 | cameras.main.lookAt(new Vector3()); 45 | 46 | const controls = { 47 | dev: new OrbitControls(cameras.dev, renderer.domElement), 48 | main: new OrbitControls(cameras.main, renderer.domElement) 49 | }; 50 | controls.main.enableDamping = true; 51 | 52 | const scene = new Scene(); 53 | 54 | const gridHelper = new GridHelper(10, 10); 55 | const axesHelper = new AxesHelper(); 56 | const cameraHelper = new CameraHelper(cameras.main); 57 | const helpers = new Group(); 58 | helpers.add(gridHelper, axesHelper, cameraHelper); 59 | scene.add(helpers); 60 | 61 | // Create particle classes 62 | const particlesNormal = new ParticlesNormal(renderer); 63 | const particles = new Particles( 64 | 5000, // total particles 65 | particlesNormal, // particles normal texture class 66 | renderer.getPixelRatio() 67 | ); 68 | scene.add(particles.mesh); 69 | 70 | function renderScene(camera, left, bottom, width, height) { 71 | left *= window.innerWidth; 72 | bottom *= window.innerHeight; 73 | width *= window.innerWidth; 74 | height *= window.innerHeight; 75 | 76 | renderer.setViewport(left, bottom, width, height); 77 | renderer.setScissor(left, bottom, width, height); 78 | 79 | renderer.render(scene, camera); 80 | } 81 | 82 | function update() { 83 | requestAnimationFrame(update); 84 | 85 | const activeCamera = guiController.cameraDebug ? cameras.dev : cameras.main; 86 | 87 | controls.main.enabled = !guiController.cameraDebug; 88 | helpers.visible = guiController.cameraDebug; 89 | controls.main.update(); 90 | 91 | // Render particle normal texture 92 | particlesNormal.render(activeCamera); 93 | 94 | if (guiController.cameraDebug) { 95 | renderScene(cameras.dev, 0, 0, 1, 1); 96 | renderScene(cameras.main, 0, 0, 0.25, 0.25); 97 | } else { 98 | renderScene(cameras.main, 0, 0, 1, 1); 99 | } 100 | 101 | renderStats.update(renderer); 102 | } 103 | 104 | function onResize() { 105 | // Update camera projections 106 | cameras.dev.aspect = window.innerWidth / window.innerHeight; 107 | cameras.main.aspect = cameras.dev.aspect; 108 | cameras.dev.updateProjectionMatrix(); 109 | cameras.main.updateProjectionMatrix(); 110 | // Set webgl context size 111 | renderer.setSize(window.innerWidth, window.innerHeight); 112 | } 113 | 114 | window.addEventListener('resize', onResize); 115 | 116 | update(); 117 | -------------------------------------------------------------------------------- /src/js/2-particles/particles/particles-normal.js: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | Mesh, 4 | SphereBufferGeometry, 5 | ShaderMaterial, 6 | PerspectiveCamera, 7 | RGBAFormat, 8 | WebGLRenderTarget 9 | } from 'three'; 10 | import { createCanvas, VECTOR_ZERO } from '../../utils'; 11 | 12 | // Render target size 13 | const TEXTURE_SIZE = 128; 14 | // Preview render target in canvas for debugging 15 | const DEBUG_CANVAS = true; 16 | 17 | export default class ParticlesNormal { 18 | constructor(renderer) { 19 | this.renderer = renderer; 20 | // Create an empty scene 21 | this.scene = new Scene(); 22 | // Create a new perspective camera 23 | this.camera = new PerspectiveCamera(60, 1, 0.01, 5); 24 | // Camera position is set the diameter of the sphere away 25 | this.camera.position.set(0, 0, 2); 26 | // Look at the center 27 | this.camera.lookAt(VECTOR_ZERO); 28 | // Create render target texture for normal map 29 | this.renderTarget = new WebGLRenderTarget(TEXTURE_SIZE, TEXTURE_SIZE, { 30 | format: RGBAFormat, 31 | stencilBuffer: false 32 | }); 33 | 34 | // Setup sphere mesh 35 | this.mesh = new Mesh( 36 | new SphereBufferGeometry(1, 32, 32), 37 | new ShaderMaterial({ 38 | vertexShader: ` 39 | varying vec3 vNormal; 40 | void main() { 41 | vNormal = normal; 42 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 43 | } 44 | `, 45 | fragmentShader: ` 46 | varying vec3 vNormal; 47 | void main() { 48 | // Pack the normal range from (-1, 1), to (0, 1) 49 | gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0); 50 | } 51 | ` 52 | }) 53 | ); 54 | this.scene.add(this.mesh); 55 | 56 | // Create debug canvases to preview the render target output 57 | // Note: Render target outputs pixels on the y-axis inverted 58 | if (DEBUG_CANVAS) { 59 | const { canvas, ctx } = createCanvas(TEXTURE_SIZE, TEXTURE_SIZE); 60 | const { canvas: canvasFlipped, ctx: ctxFlipped } = createCanvas( 61 | TEXTURE_SIZE, 62 | TEXTURE_SIZE 63 | ); 64 | this.canvas = canvas; 65 | this.ctx = ctx; 66 | this.canvasFlipped = canvasFlipped; 67 | this.ctxFlipped = ctxFlipped; 68 | 69 | this.pixelBuffer = new Uint8Array( 70 | this.renderTarget.width * this.renderTarget.height * 4 71 | ); 72 | this.imageData = this.ctxFlipped.createImageData( 73 | this.canvas.width, 74 | this.canvas.height 75 | ); 76 | 77 | Object.assign(canvas.style, { 78 | top: '0px', 79 | left: '80px', 80 | position: 'absolute', 81 | zIndex: 1000, 82 | pointerEvents: 'none', 83 | width: `${TEXTURE_SIZE / 2}px`, 84 | height: `${TEXTURE_SIZE / 2}px` 85 | }); 86 | 87 | Object.assign(canvasFlipped.style, { 88 | top: '0px', 89 | left: `${80 + TEXTURE_SIZE / 2}px`, 90 | position: 'absolute', 91 | zIndex: 1000, 92 | pointerEvents: 'none', 93 | width: `${TEXTURE_SIZE / 2}px`, 94 | height: `${TEXTURE_SIZE / 2}px` 95 | }); 96 | 97 | document.body.appendChild(canvas); 98 | document.body.appendChild(canvasFlipped); 99 | } 100 | } 101 | 102 | render(camera) { 103 | // Set the active render target 104 | this.renderer.setRenderTarget(this.renderTarget); 105 | // Copy the camera position but limit the length 106 | this.camera.position.copy(camera.position).setLength(2); 107 | // Ensure the camera is looking at the center 108 | this.camera.lookAt(VECTOR_ZERO); 109 | // Render the scene 110 | this.renderer.render(this.scene, this.camera); 111 | 112 | if (DEBUG_CANVAS) { 113 | // Output the render target pixels into the pixel buffer 114 | this.renderer.readRenderTargetPixels( 115 | this.renderTarget, 116 | 0, 117 | 0, 118 | this.renderTarget.width, 119 | this.renderTarget.height, 120 | this.pixelBuffer 121 | ); 122 | // Update the image data 123 | this.imageData.data.set(this.pixelBuffer); 124 | this.ctxFlipped.putImageData(this.imageData, 0, 0); 125 | this.ctx.save(); 126 | // Flip the canvas on the y-axis 127 | this.ctx.scale(1, -1); 128 | // Draw the image the correct way 129 | this.ctx.drawImage( 130 | this.canvasFlipped, 131 | 0, 132 | -this.canvas.height, 133 | this.canvas.width, 134 | this.canvas.height 135 | ); 136 | this.ctx.restore(); 137 | } 138 | // Reset the render target 139 | this.renderer.setRenderTarget(null); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/js/2-particles/particles/particles.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | ShaderMaterial, 5 | Vector3, 6 | Points, 7 | Math as Math3 8 | } from 'three'; 9 | import { spherePoint } from '../../utils'; 10 | import { gui } from '../../gui'; 11 | import { vertexShader, fragmentShader } from './shader.glsl'; 12 | 13 | export default class Particles { 14 | constructor(totalParticles, particlesNormal, pixelRatio) { 15 | this.config = { 16 | totalParticles, 17 | size: { 18 | min: 0.1, 19 | max: 5 20 | } 21 | }; 22 | 23 | // Create two attributes for positions and size 24 | this.attributes = { 25 | position: new BufferAttribute( 26 | new Float32Array(this.config.totalParticles * 3), 27 | 3 28 | ), 29 | size: new BufferAttribute(new Float32Array(this.config.totalParticles), 1) 30 | }; 31 | 32 | // Set initial position and scale for particles 33 | for (let i = 0; i < this.config.totalParticles; i++) { 34 | const { x, y, z } = spherePoint( 35 | 0, 36 | 0, 37 | 0, 38 | Math.random(), 39 | Math.random(), 40 | Math3.randFloat(10, 50) 41 | ); 42 | this.attributes.position.setXYZ(i, x, y, z); 43 | 44 | const size = 45 | Math3.randFloat(this.config.size.min, this.config.size.max) * 46 | pixelRatio; 47 | this.attributes.size.setX(i, size); 48 | } 49 | 50 | // Setup buffer geometry 51 | const geometry = new BufferGeometry(); 52 | geometry.addAttribute('position', this.attributes.position); 53 | geometry.addAttribute('size', this.attributes.size); 54 | 55 | // Setup custom shader material 56 | const material = new ShaderMaterial({ 57 | uniforms: { 58 | particleSize: { value: 100 }, // Scale particles uniformly 59 | lightDirection: { value: new Vector3(1, 1, 1) }, // Light direction for lambert shading 60 | normalMap: { 61 | value: particlesNormal.renderTarget.texture // Normal map 62 | } 63 | }, 64 | vertexShader, 65 | fragmentShader 66 | }); 67 | 68 | // Add gui slider to tweak light direction 69 | gui.add(material.uniforms.lightDirection.value, 'x', -1, 1).name('light x'); 70 | gui.add(material.uniforms.lightDirection.value, 'y', -1, 1).name('light y'); 71 | gui.add(material.uniforms.lightDirection.value, 'z', -1, 1).name('light z'); 72 | 73 | // Create points mesh 74 | this.mesh = new Points(geometry, material); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/js/2-particles/particles/shader.glsl.js: -------------------------------------------------------------------------------- 1 | export const vertexShader = ` 2 | attribute float size; // Per particle size attribute 3 | uniform float particleSize; // Uniform particle size (affects all) 4 | varying vec3 vPosition; // Vertex position for fragment shader 5 | 6 | void main() { 7 | // Model view position 8 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); 9 | // Scale point size based on distance 10 | gl_PointSize = size * (particleSize / length(mvPosition.xyz)); 11 | // Screen space projection 12 | gl_Position = projectionMatrix * mvPosition; 13 | // Set position varying 14 | vPosition = position; 15 | } 16 | `; 17 | 18 | export const fragmentShader = ` 19 | uniform vec3 lightDirection; 20 | uniform sampler2D normalMap; 21 | varying vec3 vPosition; 22 | 23 | // Signed distance function for 2D circle 24 | float circle(vec2 uv, vec2 pos, float rad) { 25 | float d = length(pos - uv) - rad; 26 | return step(d, 0.0); 27 | } 28 | 29 | void main() { 30 | float c = circle(vec2(0.5), gl_PointCoord.xy, 0.5); 31 | // Discard any pixels outside of circle 32 | // if (c == 0.0) discard; 33 | 34 | vec4 outgoingColor = vec4(1.0); 35 | 36 | // Sample normal from texture, y coords are inverted from render target 37 | vec3 normal = texture2D(normalMap, vec2(gl_PointCoord.x, 1.0 - gl_PointCoord.y)).rgb * 2.0 - 1.0; 38 | // Second normal based on spherical particle position 39 | vec3 normal2 = normalize(vPosition); 40 | 41 | // Half lambert lighting model 42 | float intensity = max(0.2, dot(normal, lightDirection) * 0.5 + 0.5); 43 | float intensty2 = max(0.5, dot(normal2, lightDirection) * 0.5 + 0.5); 44 | 45 | // Base color on normal 46 | vec3 color = vec3(normal2 * 0.5 + 0.5); 47 | 48 | // Apply lambert intensity 49 | color *= intensity; 50 | color *= intensty2; 51 | 52 | // Set outgoing color 53 | // outgoingColor.rgb = color; 54 | 55 | gl_FragColor = outgoingColor; 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /src/js/3-context-resizing/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | WebGLRenderer, 3 | PerspectiveCamera, 4 | Scene, 5 | GridHelper, 6 | AxesHelper, 7 | Vector3, 8 | Vector2, 9 | CameraHelper, 10 | Group 11 | } from 'three'; 12 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 13 | import { renderStats } from '../stats'; 14 | import { guiController } from '../gui'; 15 | import Particles from '../2-particles/particles/particles'; 16 | import ParticlesNormal from '../2-particles/particles/particles-normal'; 17 | 18 | // Fullscreen context mode to preview performance difference 19 | const USE_FULLSCREEN = false; 20 | // Max render buffer dimensions 1280 * 720 21 | const MAX_FRAME_BUFFER_SIZE = new Vector2(1280, 720); 22 | // Caculate square root 23 | const BASE_SIZE = Math.sqrt(MAX_FRAME_BUFFER_SIZE.x * MAX_FRAME_BUFFER_SIZE.y); 24 | // Max base dimension 25 | const MAX_SIZE = BASE_SIZE * BASE_SIZE; 26 | 27 | // Debug elements 28 | const renderSizeBase = document.querySelector('.base'); 29 | const renderSizeBaseText = renderSizeBase.querySelector('.size'); 30 | const renderSizeResized = document.querySelector('.resized'); 31 | const renderSizeResizedText = renderSizeResized.querySelector('.size'); 32 | 33 | // Add infos for context dimensions 34 | renderSizeBase.style.width = `${MAX_FRAME_BUFFER_SIZE.x}px`; 35 | renderSizeBase.style.height = `${MAX_FRAME_BUFFER_SIZE.y}px`; 36 | renderSizeResized.style.left = `${MAX_FRAME_BUFFER_SIZE.x / 2}px`; 37 | renderSizeBaseText.innerHTML = `${MAX_FRAME_BUFFER_SIZE.x}x${MAX_FRAME_BUFFER_SIZE.y}`; 38 | 39 | // Calculate the context size based on the max dimension 40 | function calculateRendererSize(windowWidth, windowHeight) { 41 | let width = windowWidth; 42 | let height = windowHeight; 43 | if (USE_FULLSCREEN) { 44 | return { 45 | width, 46 | height 47 | }; 48 | } 49 | if (windowWidth * windowHeight > MAX_SIZE) { 50 | const ratio = height / width; 51 | width = BASE_SIZE; 52 | height = Math.floor(BASE_SIZE * ratio); 53 | const newSize = width * height; 54 | const scalar = Math.sqrt(MAX_SIZE / newSize); 55 | width = Math.floor(width * scalar); 56 | height = Math.floor(height * scalar); 57 | } 58 | return { 59 | width, 60 | height 61 | }; 62 | } 63 | 64 | const renderer = new WebGLRenderer({ antialias: true }); 65 | renderer.debug.checkShaderErrors = true; 66 | 67 | renderer.setScissorTest(true); 68 | 69 | let renderSize = calculateRendererSize(window.innerWidth, window.innerHeight); 70 | renderer.setPixelRatio(window.devicePixelRatio); 71 | renderer.setSize(renderSize.width, renderSize.height); 72 | document.body.appendChild(renderer.domElement); 73 | 74 | const cameras = { 75 | dev: new PerspectiveCamera( 76 | 65, 77 | renderSize.width / renderSize.height, 78 | 0.1, 79 | 1000 80 | ), 81 | main: new PerspectiveCamera( 82 | 65, 83 | renderSize.width / renderSize.height, 84 | 0.1, 85 | 1000 86 | ) 87 | }; 88 | 89 | cameras.dev.position.set(20, 10, 20); 90 | cameras.dev.lookAt(new Vector3()); 91 | 92 | cameras.main.position.set(0, 10, 20); 93 | cameras.main.lookAt(new Vector3()); 94 | 95 | const controls = { 96 | dev: new OrbitControls(cameras.dev, renderer.domElement), 97 | main: new OrbitControls(cameras.main, renderer.domElement) 98 | }; 99 | controls.main.enableDamping = true; 100 | 101 | const scene = new Scene(); 102 | 103 | // Add some debug helpers 104 | const gridHelper = new GridHelper(10, 10); 105 | const axesHelper = new AxesHelper(); 106 | const cameraHelper = new CameraHelper(cameras.main); 107 | const helpers = new Group(); 108 | helpers.add(gridHelper, axesHelper, cameraHelper); 109 | scene.add(helpers); 110 | 111 | // Create particle classes 112 | const particlesNormal = new ParticlesNormal(renderer); 113 | const particles = new Particles( 114 | 5000, 115 | particlesNormal, 116 | renderer.getPixelRatio() 117 | ); 118 | scene.add(particles.mesh); 119 | 120 | function renderScene(camera, left, bottom, width, height) { 121 | left *= renderSize.width; 122 | bottom *= renderSize.height; 123 | width *= renderSize.width; 124 | height *= renderSize.height; 125 | 126 | renderer.setViewport(left, bottom, width, height); 127 | renderer.setScissor(left, bottom, width, height); 128 | 129 | renderer.render(scene, camera); 130 | } 131 | 132 | function update() { 133 | requestAnimationFrame(update); 134 | 135 | const activeCamera = guiController.cameraDebug ? cameras.dev : cameras.main; 136 | 137 | controls.main.enabled = !guiController.cameraDebug; 138 | helpers.visible = guiController.cameraDebug; 139 | controls.main.update(); 140 | 141 | // Render particle normal texture 142 | particlesNormal.render(activeCamera); 143 | 144 | if (guiController.cameraDebug) { 145 | renderScene(cameras.dev, 0, 0, 1, 1); 146 | renderScene(cameras.main, 0, 0, 0.25, 0.25); 147 | } else { 148 | renderScene(cameras.main, 0, 0, 1, 1); 149 | } 150 | 151 | renderStats.update(renderer); 152 | } 153 | 154 | function onResize() { 155 | // Calculate new render size 156 | renderSize = calculateRendererSize(window.innerWidth, window.innerHeight); 157 | renderer.setSize(renderSize.width, renderSize.height); 158 | 159 | // Scale to window 160 | renderer.domElement.style.width = `${window.innerWidth}px`; 161 | renderer.domElement.style.height = `${window.innerHeight}px`; 162 | 163 | // Update debug elements 164 | renderSizeResized.style.width = `${renderSize.width}px`; 165 | renderSizeResized.style.height = `${renderSize.height}px`; 166 | renderSizeResizedText.innerHTML = `${renderSize.width}x${renderSize.height}`; 167 | 168 | // Update camera projections 169 | cameras.dev.aspect = renderSize.width / renderSize.height; 170 | cameras.main.aspect = cameras.dev.aspect; 171 | cameras.dev.updateProjectionMatrix(); 172 | cameras.main.updateProjectionMatrix(); 173 | } 174 | 175 | window.addEventListener('resize', onResize); 176 | 177 | onResize(); 178 | update(); 179 | -------------------------------------------------------------------------------- /src/js/4-gpu-profiling/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | WebGLRenderer, 3 | PerspectiveCamera, 4 | Scene, 5 | GridHelper, 6 | AxesHelper, 7 | Vector3, 8 | Vector2, 9 | CameraHelper, 10 | Group 11 | } from 'three'; 12 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 13 | import { renderStats } from '../stats'; 14 | import { guiController, gui } from '../gui'; 15 | import Particles from '../2-particles/particles/particles'; 16 | import ParticlesNormal from '../2-particles/particles/particles-normal'; 17 | import graphicsMode, { GRAPHICS_HIGH } from './profiler'; 18 | 19 | const USE_FULLSCREEN = false; 20 | const MAX_FRAME_BUFFER_SIZE = new Vector2(1280, 720); 21 | const BASE_SIZE = Math.sqrt(MAX_FRAME_BUFFER_SIZE.x * MAX_FRAME_BUFFER_SIZE.y); 22 | const MAX_SIZE = BASE_SIZE * BASE_SIZE; 23 | 24 | // Display the graphics mode on the gui 25 | guiController.graphics = graphicsMode(); 26 | gui.add(guiController, 'graphics'); 27 | 28 | function calculateRendererSize(windowWidth, windowHeight) { 29 | let width = windowWidth; 30 | let height = windowHeight; 31 | if (USE_FULLSCREEN) { 32 | return { 33 | width, 34 | height 35 | }; 36 | } 37 | if (windowWidth * windowHeight > MAX_SIZE) { 38 | const ratio = height / width; 39 | width = BASE_SIZE; 40 | height = Math.floor(BASE_SIZE * ratio); 41 | const newSize = width * height; 42 | const scalar = Math.sqrt(MAX_SIZE / newSize); 43 | width = Math.floor(width * scalar); 44 | height = Math.floor(height * scalar); 45 | } 46 | return { 47 | width, 48 | height 49 | }; 50 | } 51 | 52 | const renderer = new WebGLRenderer({ 53 | antialias: graphicsMode() === GRAPHICS_HIGH 54 | }); 55 | renderer.debug.checkShaderErrors = true; 56 | 57 | renderer.setScissorTest(true); 58 | 59 | let renderSize = calculateRendererSize(window.innerWidth, window.innerHeight); 60 | renderer.setPixelRatio(window.devicePixelRatio); 61 | renderer.setSize(renderSize.width, renderSize.height); 62 | document.body.appendChild(renderer.domElement); 63 | 64 | const cameras = { 65 | dev: new PerspectiveCamera( 66 | 65, 67 | renderSize.width / renderSize.height, 68 | 0.1, 69 | 1000 70 | ), 71 | main: new PerspectiveCamera( 72 | 65, 73 | renderSize.width / renderSize.height, 74 | 0.1, 75 | 1000 76 | ) 77 | }; 78 | 79 | cameras.dev.position.set(20, 10, 20); 80 | cameras.dev.lookAt(new Vector3()); 81 | 82 | cameras.main.position.set(0, 10, 20); 83 | cameras.main.lookAt(new Vector3()); 84 | 85 | const controls = { 86 | dev: new OrbitControls(cameras.dev, renderer.domElement), 87 | main: new OrbitControls(cameras.main, renderer.domElement) 88 | }; 89 | controls.main.enableDamping = true; 90 | 91 | const scene = new Scene(); 92 | 93 | // Add some debug helpers 94 | const gridHelper = new GridHelper(10, 10); 95 | const axesHelper = new AxesHelper(); 96 | const cameraHelper = new CameraHelper(cameras.main); 97 | const helpers = new Group(); 98 | helpers.add(gridHelper, axesHelper, cameraHelper); 99 | scene.add(helpers); 100 | 101 | // Create particle classes 102 | const particlesNormal = new ParticlesNormal(renderer); 103 | // Set the amount of particles depending on graphics setting 104 | const totalParticles = graphicsMode() === GRAPHICS_HIGH ? 5000 : 2500; 105 | const particles = new Particles( 106 | totalParticles, 107 | particlesNormal, 108 | renderer.getPixelRatio() 109 | ); 110 | scene.add(particles.mesh); 111 | 112 | function renderScene(camera, left, bottom, width, height) { 113 | left *= renderSize.width; 114 | bottom *= renderSize.height; 115 | width *= renderSize.width; 116 | height *= renderSize.height; 117 | 118 | renderer.setViewport(left, bottom, width, height); 119 | renderer.setScissor(left, bottom, width, height); 120 | 121 | renderer.render(scene, camera); 122 | } 123 | 124 | function update() { 125 | requestAnimationFrame(update); 126 | 127 | const activeCamera = guiController.cameraDebug ? cameras.dev : cameras.main; 128 | 129 | controls.main.enabled = !guiController.cameraDebug; 130 | helpers.visible = guiController.cameraDebug; 131 | controls.main.update(); 132 | 133 | // Render particle normal texture 134 | particlesNormal.render(activeCamera); 135 | 136 | if (guiController.cameraDebug) { 137 | renderScene(cameras.dev, 0, 0, 1, 1); 138 | renderScene(cameras.main, 0, 0, 0.25, 0.25); 139 | } else { 140 | renderScene(cameras.main, 0, 0, 1, 1); 141 | } 142 | 143 | renderStats.update(renderer); 144 | } 145 | 146 | function onResize() { 147 | // Calculate new render size 148 | renderSize = calculateRendererSize(window.innerWidth, window.innerHeight); 149 | renderer.setSize(renderSize.width, renderSize.height); 150 | 151 | // Scale to window 152 | renderer.domElement.style.width = `${window.innerWidth}px`; 153 | renderer.domElement.style.height = `${window.innerHeight}px`; 154 | 155 | // Update camera projection 156 | cameras.dev.aspect = renderSize.width / renderSize.height; 157 | cameras.main.aspect = cameras.dev.aspect; 158 | cameras.dev.updateProjectionMatrix(); 159 | cameras.main.updateProjectionMatrix(); 160 | } 161 | 162 | window.addEventListener('resize', onResize); 163 | 164 | onResize(); 165 | update(); 166 | -------------------------------------------------------------------------------- /src/js/4-gpu-profiling/profiler.js: -------------------------------------------------------------------------------- 1 | import { getGPUTier } from 'detect-gpu'; 2 | 3 | const gpuTier = getGPUTier(); 4 | 5 | export const GRAPHICS_HIGH = 'GRAPHICS_HIGH'; 6 | export const GRAPHICS_NORMAL = 'GRAPHICS_NORMAL'; 7 | 8 | // Determine graphics setting based on gpu tier 9 | export default function graphicsMode() { 10 | switch (gpuTier.tier) { 11 | case 'GPU_DESKTOP_TIER_3': 12 | case 'GPU_DESKTOP_TIER_2': 13 | return GRAPHICS_HIGH; 14 | case 'GPU_DESKTOP_TIER_1': 15 | default: 16 | return GRAPHICS_NORMAL; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/js/5-post-processing/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | WebGLRenderer, 3 | PerspectiveCamera, 4 | Scene, 5 | GridHelper, 6 | AxesHelper, 7 | Vector3, 8 | Vector2, 9 | CameraHelper, 10 | Group, 11 | Clock 12 | } from 'three'; 13 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 14 | import { renderStats } from '../stats'; 15 | import { guiController, gui } from '../gui'; 16 | import Particles from '../2-particles/particles/particles'; 17 | import ParticlesNormal from '../2-particles/particles/particles-normal'; 18 | import graphicsMode, { GRAPHICS_HIGH } from '../4-gpu-profiling/profiler'; 19 | import PostProcessing from './post-processing'; 20 | 21 | const USE_FULLSCREEN = false; 22 | const MAX_FRAME_BUFFER_SIZE = new Vector2(1280, 720); 23 | const BASE_SIZE = Math.sqrt(MAX_FRAME_BUFFER_SIZE.x * MAX_FRAME_BUFFER_SIZE.y); 24 | const MAX_SIZE = BASE_SIZE * BASE_SIZE; 25 | 26 | guiController.graphics = graphicsMode(); 27 | gui.add(guiController, 'graphics'); 28 | 29 | function calculateRendererSize(windowWidth, windowHeight) { 30 | let width = windowWidth; 31 | let height = windowHeight; 32 | if (USE_FULLSCREEN) { 33 | return { 34 | width, 35 | height 36 | }; 37 | } 38 | if (windowWidth * windowHeight > MAX_SIZE) { 39 | const ratio = height / width; 40 | width = BASE_SIZE; 41 | height = Math.floor(BASE_SIZE * ratio); 42 | const newSize = width * height; 43 | const scalar = Math.sqrt(MAX_SIZE / newSize); 44 | width = Math.floor(width * scalar); 45 | height = Math.floor(height * scalar); 46 | } 47 | return { 48 | width, 49 | height 50 | }; 51 | } 52 | 53 | const renderer = new WebGLRenderer({ 54 | antialias: graphicsMode() === GRAPHICS_HIGH 55 | }); 56 | renderer.debug.checkShaderErrors = true; 57 | 58 | renderer.setScissorTest(true); 59 | 60 | let renderSize = calculateRendererSize(window.innerWidth, window.innerHeight); 61 | renderer.setPixelRatio(window.devicePixelRatio); 62 | renderer.setSize(renderSize.width, renderSize.height); 63 | document.body.appendChild(renderer.domElement); 64 | 65 | // Create new post processing class 66 | const postProcessing = new PostProcessing( 67 | renderer, 68 | renderSize.width, 69 | renderSize.height 70 | ); 71 | 72 | const clock = new Clock(true); 73 | 74 | const cameras = { 75 | dev: new PerspectiveCamera( 76 | 65, 77 | renderSize.width / renderSize.height, 78 | 0.1, 79 | 1000 80 | ), 81 | main: new PerspectiveCamera( 82 | 65, 83 | renderSize.width / renderSize.height, 84 | 0.1, 85 | 1000 86 | ) 87 | }; 88 | 89 | cameras.dev.position.set(20, 10, 20); 90 | cameras.dev.lookAt(new Vector3()); 91 | 92 | cameras.main.position.set(0, 10, 20); 93 | cameras.main.lookAt(new Vector3()); 94 | 95 | const controls = { 96 | dev: new OrbitControls(cameras.dev, renderer.domElement), 97 | main: new OrbitControls(cameras.main, renderer.domElement) 98 | }; 99 | controls.main.enableDamping = true; 100 | 101 | const scene = new Scene(); 102 | 103 | // Add some debug helpers 104 | const gridHelper = new GridHelper(10, 10); 105 | const axesHelper = new AxesHelper(); 106 | const cameraHelper = new CameraHelper(cameras.main); 107 | const helpers = new Group(); 108 | helpers.add(gridHelper, axesHelper, cameraHelper); 109 | scene.add(helpers); 110 | 111 | // Create particle classes 112 | const particlesNormal = new ParticlesNormal(renderer); 113 | const totalParticles = graphicsMode() === GRAPHICS_HIGH ? 5000 : 2500; 114 | const particles = new Particles( 115 | totalParticles, 116 | particlesNormal, 117 | renderer.getPixelRatio() 118 | ); 119 | scene.add(particles.mesh); 120 | 121 | function renderScene( 122 | camera, 123 | left, 124 | bottom, 125 | width, 126 | height, 127 | delta, 128 | usePostProcessing = false 129 | ) { 130 | left *= renderSize.width; 131 | bottom *= renderSize.height; 132 | width *= renderSize.width; 133 | height *= renderSize.height; 134 | 135 | renderer.setViewport(left, bottom, width, height); 136 | renderer.setScissor(left, bottom, width, height); 137 | 138 | if (usePostProcessing) { 139 | postProcessing.render(scene, camera, delta); 140 | } else { 141 | renderer.render(scene, camera); 142 | } 143 | } 144 | 145 | function update() { 146 | requestAnimationFrame(update); 147 | 148 | const activeCamera = guiController.cameraDebug ? cameras.dev : cameras.main; 149 | 150 | controls.main.enabled = !guiController.cameraDebug; 151 | helpers.visible = guiController.cameraDebug; 152 | controls.main.update(); 153 | 154 | // Render particle normal texture 155 | particlesNormal.render(activeCamera); 156 | 157 | const delta = clock.getDelta(); 158 | 159 | if (guiController.cameraDebug) { 160 | renderScene(cameras.dev, 0, 0, 1, 1, delta); 161 | renderScene(cameras.main, 0, 0, 0.25, delta, 0.25); 162 | } else { 163 | // Only render post processing on main camera 164 | renderScene(cameras.main, 0, 0, 1, 1, delta, true); 165 | } 166 | 167 | renderStats.update(renderer); 168 | } 169 | 170 | function onResize() { 171 | // Set new render size 172 | renderSize = calculateRendererSize(window.innerWidth, window.innerHeight); 173 | renderer.setSize(renderSize.width, renderSize.height); 174 | postProcessing.resize(renderSize.width, renderSize.height); 175 | 176 | // Scale to window 177 | renderer.domElement.style.width = `${window.innerWidth}px`; 178 | renderer.domElement.style.height = `${window.innerHeight}px`; 179 | 180 | // Update camera projections 181 | cameras.dev.aspect = renderSize.width / renderSize.height; 182 | cameras.main.aspect = cameras.dev.aspect; 183 | cameras.dev.updateProjectionMatrix(); 184 | cameras.main.updateProjectionMatrix(); 185 | } 186 | 187 | window.addEventListener('resize', onResize); 188 | 189 | onResize(); 190 | update(); 191 | -------------------------------------------------------------------------------- /src/js/5-post-processing/post-processing.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferGeometry, 3 | BufferAttribute, 4 | Mesh, 5 | ShaderMaterial, 6 | WebGLRenderTarget, 7 | Vector2, 8 | LinearFilter, 9 | NearestFilter, 10 | RGBFormat, 11 | Scene, 12 | OrthographicCamera 13 | } from 'three'; 14 | import { gui } from '../gui'; 15 | import { vertexShader, fragmentShader } from './shader.glsl'; 16 | 17 | // https://github.com/mikolalysenko/a-big-triangle 18 | 19 | export default class PostProcessing { 20 | constructor(renderer, width, height) { 21 | this.renderer = renderer; 22 | 23 | const pixelRatio = renderer.getPixelRatio(); 24 | 25 | // Construct a big triangle that covers screen space 26 | const geometry = new BufferGeometry(); 27 | const attribute = new BufferAttribute( 28 | new Float32Array([-1, -1, 0, -1, 3, 0, 3, -1, 0]), 29 | 3 30 | ); 31 | geometry.addAttribute('position', attribute); 32 | geometry.setIndex([0, 2, 1]); 33 | 34 | // Setup the render target 35 | // Note: We want to use the same pixel ratio as the webgl renderer 36 | this.renderTarget = new WebGLRenderTarget( 37 | width * pixelRatio, 38 | height * pixelRatio, 39 | { 40 | minFilter: LinearFilter, 41 | magFilter: NearestFilter, 42 | format: RGBFormat, 43 | stencilBuffer: false 44 | } 45 | ); 46 | 47 | // Setup the material with some noise uniforms 48 | const material = new ShaderMaterial({ 49 | uniforms: { 50 | textureMap: { 51 | type: 't', 52 | value: this.renderTarget.texture 53 | }, 54 | resolution: { 55 | value: new Vector2(this.renderTarget.width, this.renderTarget.height) 56 | }, 57 | time: { 58 | value: 0 59 | }, 60 | noiseSpeed: { value: 0.18 }, 61 | noiseAmount: { value: 0.35 } 62 | }, 63 | vertexShader, 64 | fragmentShader 65 | }); 66 | 67 | // Create an empty scene and orthographic camera 68 | this.scene = new Scene(); 69 | this.camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1); 70 | 71 | this.mesh = new Mesh(geometry, material); 72 | // Mesh won't be moving so we can turn off the matrix update 73 | this.mesh.matrixAutoUpdate = false; 74 | this.mesh.updateMatrix(); 75 | 76 | this.scene.add(this.mesh); 77 | 78 | gui 79 | .add(this.mesh.material.uniforms.noiseAmount, 'value', 0, 1) 80 | .name('noiseAmount'); 81 | gui 82 | .add(this.mesh.material.uniforms.noiseSpeed, 'value', 0, 1) 83 | .name('noiseSpeed'); 84 | } 85 | 86 | resize(width, height) { 87 | // Resize the render target when the resolution changes 88 | const pixelRatio = this.renderer.getPixelRatio(); 89 | this.renderTarget.setSize(width * pixelRatio, height * pixelRatio); 90 | this.mesh.material.uniforms.resolution.value.x = width * pixelRatio; 91 | this.mesh.material.uniforms.resolution.value.y = height * pixelRatio; 92 | } 93 | 94 | render(scene, camera, delta) { 95 | // Update time uniform 96 | this.mesh.material.uniforms.time.value += delta; 97 | // Set the active render target 98 | this.renderer.setRenderTarget(this.renderTarget); 99 | // Render the scene into the quad 100 | this.renderer.render(scene, camera); 101 | // Reset the render target 102 | this.renderer.setRenderTarget(null); 103 | // Render the quad fullscreen 104 | this.renderer.render(this.scene, this.camera); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/js/5-post-processing/shader.glsl.js: -------------------------------------------------------------------------------- 1 | export const vertexShader = ` 2 | void main() { 3 | // Screen space projection 4 | gl_Position = vec4(position, 1.0); 5 | } 6 | `; 7 | 8 | export const fragmentShader = ` 9 | uniform sampler2D textureMap; // Rendered scene texture 10 | uniform vec2 resolution; // Current resolution 11 | uniform float time; // Elapsed time 12 | 13 | // Noise 14 | uniform float noiseSpeed; 15 | uniform float noiseAmount; 16 | 17 | // Random noise 18 | float random(vec2 n, float offset) { 19 | return 0.5 - fract(sin(dot(n.xy + vec2(offset, 0.0), vec2(12.9898, 78.233)))* 43758.5453); 20 | } 21 | 22 | void main() { 23 | // Get uv based on frag coord 24 | vec2 uv = gl_FragCoord.xy / resolution; 25 | 26 | // Sample color from rendered scene texture 27 | vec4 outgoingColor = texture2D(textureMap, uv); 28 | 29 | // Add noise 30 | outgoingColor += vec4(vec3(noiseAmount * random(uv, 0.00001 * noiseSpeed * time)), 1.0); 31 | 32 | // Output final color 33 | gl_FragColor = outgoingColor; 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /src/js/gui.js: -------------------------------------------------------------------------------- 1 | import dat from 'dat.gui'; 2 | import queryString from 'query-string'; 3 | 4 | // Setup dat.gui 5 | 6 | const gui = new dat.GUI(); 7 | // dat.GUI.toggleHide(); 8 | 9 | const queries = queryString.parse(window.location.search); 10 | 11 | function getQuery(query) { 12 | return queries[query]; 13 | } 14 | 15 | const setQuery = (query, val) => { 16 | const newQueries = Object.assign({}, queries, { 17 | [query]: val 18 | }); 19 | const stringified = queryString.stringify(newQueries); 20 | 21 | const url = `${window.location.pathname}?${stringified}`; 22 | window.location.href = url; 23 | }; 24 | 25 | const guiController = { 26 | cameraDebug: getQuery('cameraDebug') === 'true' 27 | }; 28 | 29 | gui.add(guiController, 'cameraDebug').onChange(val => { 30 | setQuery('cameraDebug', val); 31 | }); 32 | 33 | export { gui, guiController, getQuery, setQuery }; 34 | -------------------------------------------------------------------------------- /src/js/render-stats.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { WebGLRenderer } from 'three'; 4 | 5 | // https://github.com/jeromeetienne/threex.rendererstats 6 | 7 | /** 8 | * @author mrdoob / http://mrdoob.com/ 9 | * @author jetienne / http://jetienne.com/ 10 | */ 11 | /** @namespace */ 12 | var THREEx = THREEx || {}; 13 | 14 | /** 15 | * provide info on THREE.WebGLRenderer 16 | * 17 | * @param {Object} renderer the renderer to update 18 | * @param {Object} Camera the camera to update 19 | */ 20 | THREEx.RendererStats = function() { 21 | var msMin = 100; 22 | var msMax = 0; 23 | 24 | var container = document.createElement('div'); 25 | container.style.cssText = 26 | 'width:80px;opacity:0.9;cursor:pointer;z-index:100000;top:48px;position:absolute;'; 27 | 28 | var msDiv = document.createElement('div'); 29 | msDiv.style.cssText = 30 | 'padding:0 0 3px 3px;text-align:left;background-color:rgb(0, 0, 0);'; 31 | container.appendChild(msDiv); 32 | 33 | var msText = document.createElement('div'); 34 | msText.style.cssText = 35 | 'color:rgb(255, 255, 255);font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 36 | msText.innerHTML = 'WebGLRenderer'; 37 | msDiv.appendChild(msText); 38 | 39 | var msTexts = []; 40 | var nLines = 9; 41 | for (var i = 0; i < nLines; i++) { 42 | msTexts[i] = document.createElement('div'); 43 | msTexts[i].style.cssText = 44 | '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'; 45 | msDiv.appendChild(msTexts[i]); 46 | msTexts[i].innerHTML = '-'; 47 | } 48 | 49 | var lastTime = Date.now(); 50 | return { 51 | domElement: container, 52 | 53 | update: function(webglRenderer) { 54 | // sanity check 55 | console.assert(webglRenderer instanceof WebGLRenderer); 56 | 57 | // refresh only 30time per second 58 | if (Date.now() - lastTime < 1000 / 30) return; 59 | lastTime = Date.now(); 60 | 61 | var i = 0; 62 | msTexts[i++].textContent = '=== Memory ==='; 63 | msTexts[i++].textContent = 64 | 'Programs: ' + webglRenderer.info.programs.length; 65 | msTexts[i++].textContent = 66 | 'Geometries: ' + webglRenderer.info.memory.geometries; 67 | msTexts[i++].textContent = 68 | 'Textures: ' + webglRenderer.info.memory.textures; 69 | 70 | msTexts[i++].textContent = '=== Render ==='; 71 | msTexts[i++].textContent = 'Calls: ' + webglRenderer.info.render.calls; 72 | msTexts[i++].textContent = 73 | 'Triangles: ' + webglRenderer.info.render.triangles; 74 | msTexts[i++].textContent = 'Lines: ' + webglRenderer.info.render.lines; 75 | msTexts[i++].textContent = 'Points: ' + webglRenderer.info.render.points; 76 | } 77 | }; 78 | }; 79 | 80 | export default THREEx.RendererStats; 81 | 82 | /* eslint-enable */ 83 | -------------------------------------------------------------------------------- /src/js/stats.js: -------------------------------------------------------------------------------- 1 | import RenderStats from './render-stats'; 2 | 3 | // Setup render stats and fps stats 4 | 5 | const stats = require('@jam3/stats')(); 6 | 7 | stats.domElement.style.cssText = 'position:fixed;left:0;top:0;z-index:10000'; 8 | 9 | export const renderStats = new RenderStats(); 10 | 11 | if (document.body !== null) document.body.appendChild(renderStats.domElement); 12 | -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | // Vector zero constant 4 | export const VECTOR_ZERO = new Vector3(); 5 | 6 | /** 7 | * createCanvas 8 | * Create and return a canvas by width and height 9 | * 10 | * @export 11 | * @param {number} width 12 | * @param {number} height 13 | * @returns {object} 14 | */ 15 | export function createCanvas(width, height) { 16 | const canvas = document.createElement('canvas'); 17 | canvas.width = width; 18 | canvas.height = height; 19 | const ctx = canvas.getContext('2d'); 20 | return { 21 | ctx, 22 | canvas 23 | }; 24 | } 25 | 26 | /** 27 | * spherePoint 28 | * Return a spherical point based on uv 29 | * https://stackoverflow.com/questions/5531827/random-point-on-a-given-sphere 30 | * 31 | * @export 32 | * @param {number} x0 33 | * @param {number} y0 34 | * @param {number} z0 35 | * @param {number} u 36 | * @param {number} v 37 | * @param {number} radius 38 | * @returns {object} 39 | */ 40 | export function spherePoint(x0, y0, z0, u, v, radius) { 41 | const theta = 2 * Math.PI * u; 42 | const phi = Math.acos(2 * v - 1); 43 | const x = x0 + radius * Math.sin(phi) * Math.cos(theta); 44 | const y = y0 + radius * Math.sin(phi) * Math.sin(theta); 45 | const z = z0 + radius * Math.cos(phi); 46 | return { x, y, z }; 47 | } 48 | -------------------------------------------------------------------------------- /src/templates/1-environment.pug: -------------------------------------------------------------------------------- 1 | extends includes/_sketch 2 | 3 | block script 4 | script(src='js/1-environment.js') 5 | -------------------------------------------------------------------------------- /src/templates/2-particles.pug: -------------------------------------------------------------------------------- 1 | extends includes/_sketch 2 | 3 | block script 4 | script(src='js/2-particles.js') 5 | -------------------------------------------------------------------------------- /src/templates/3-context-resizing.pug: -------------------------------------------------------------------------------- 1 | extends includes/_sketch 2 | 3 | block content 4 | style. 5 | .render-size { 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | z-index: 1000; 10 | pointer-events: none; 11 | display: flex; 12 | align-items: center; 13 | flex-direction: column; 14 | justify-content: center; 15 | letter-spacing: 1px; 16 | color: black; 17 | transform: scale(0.5); 18 | transform-origin: 0 0; 19 | font-size: 50px; 20 | } 21 | .base { 22 | background-color: rgba(255, 0, 255, 0.8); 23 | } 24 | .resized { 25 | background-color: rgba(255, 255, 255, 0.8); 26 | } 27 | 28 | .render-size.base 29 | .text Max Frame Buffer Size 30 | .size 0x0 31 | .render-size.resized 32 | .text Resized Frame Buffer Size 33 | .size 0x0 34 | 35 | block script 36 | script(src='js/3-context-resizing.js') 37 | -------------------------------------------------------------------------------- /src/templates/4-gpu-profiling.pug: -------------------------------------------------------------------------------- 1 | extends includes/_sketch 2 | 3 | block script 4 | script(src='js/4-gpu-profiling.js') 5 | -------------------------------------------------------------------------------- /src/templates/5-post-processing.pug: -------------------------------------------------------------------------------- 1 | extends includes/_sketch 2 | 3 | block script 4 | script(src='js/5-post-processing.js') 5 | -------------------------------------------------------------------------------- /src/templates/includes/_layout.pug: -------------------------------------------------------------------------------- 1 | include _partials 2 | 3 | doctype html 4 | html 5 | +head 6 | body.index 7 | block content 8 | block script 9 | -------------------------------------------------------------------------------- /src/templates/includes/_partials.pug: -------------------------------------------------------------------------------- 1 | mixin head 2 | head 3 | meta(charset='utf-8') 4 | title Creative Coding Meetup 5 | meta(name="viewport" content="width=device-width, initial-scale=1") 6 | link(rel='stylesheet', href='style.css') 7 | -------------------------------------------------------------------------------- /src/templates/includes/_sketch.pug: -------------------------------------------------------------------------------- 1 | include _partials 2 | 3 | doctype html 4 | html 5 | +head 6 | body.example 7 | block content 8 | block script 9 | -------------------------------------------------------------------------------- /src/templates/index.pug: -------------------------------------------------------------------------------- 1 | extends includes/_layout 2 | 3 | block content 4 | header 5 | h1 Jam3 Creative Coding Meetup 6 | p.description WebGL workflows for creativity and performance 7 | nav.nav 8 | for category, key in sketches 9 | .nav__item 10 | h3 #{key} 11 | ul 12 | for sketch in category 13 | li 14 | a(href=`${sketch.id}.html`) #{sketch.title} 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | function isDir(file) { 5 | return fs.statSync(path.join('./src/js/', file)).isDirectory(); 6 | } 7 | const dirs = fs.readdirSync('./src/js/').filter(isDir); 8 | 9 | const entries = {}; 10 | dirs.forEach(dir => { 11 | entries[dir] = `./src/js/${dir}/index.js`; 12 | }); 13 | 14 | module.exports = { 15 | entry: entries, 16 | devtool: 'source-map', 17 | output: { 18 | path: path.join(process.cwd(), 'build/js'), 19 | filename: '[name].js' 20 | }, 21 | module: { 22 | loaders: [ 23 | { 24 | test: /\.js$/, 25 | exclude: /node_modules/, 26 | loader: 'babel-loader', 27 | query: { 28 | presets: ['es2015', 'stage-0'] 29 | } 30 | } 31 | ] 32 | }, 33 | stats: 'errors-only' 34 | }; 35 | --------------------------------------------------------------------------------