├── .eslintrc ├── .gitignore ├── .parcelrc ├── LICENSE ├── README.md ├── assets ├── mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.mp4 ├── mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.png └── pexels-photo-1633970.jpg ├── docs ├── README.md ├── index.0dd60020.js ├── index.0dd60020.js.map ├── index.28cb5519.css ├── index.28cb5519.css.map ├── index.html ├── mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.a5b0f0b5.mp4 └── pexels-photo-1633970.fd6d4325.jpg ├── index.html ├── js ├── ChromaKeyMaterial.js └── constants.js ├── package.json ├── styles ├── _reset.scss └── app.scss ├── ts ├── app.ts ├── index.ts └── parcel.d.ts ├── tsconfig.json └── types ├── ChromaKeyMaterial.d.ts ├── ChromaKeyMaterial.d.ts.map ├── constants.d.ts └── constants.d.ts.map /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "google", 3 | "env": { 4 | "browser": true, 5 | "es2021": true 6 | }, 7 | "parserOptions": { 8 | "parser": "@typescript-eslint/parser", 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "parser": "@typescript-eslint/parser", 13 | "rules": { 14 | "no-unused-vars": "warn", 15 | "require-jsdoc": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .parcel-cache 2 | node_modules 3 | package-lock.json 4 | .sass-cache 5 | dist/ 6 | 7 | # System Files 8 | .DS_Store 9 | Thumbs.db 10 | -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "transformers": { 4 | "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 drinkspiller 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 | 2 | # ChromaKeyMaterial 3 | 4 | A THREE.JS ShaderMaterial that removes a specified color (e.g. green screen) from a video or image texture. 5 | 6 | Shader code by https://github.com/Mugen87 on [THREE.js forum](https://discourse.threejs.org/t/production-ready-green-screen-with-three-js/23113/2). 7 | Inspired by https://github.com/hawksley/Threex.chromakey 8 | 9 | ## Source 10 | ChromaKeyMaterial.js is in `/js` directory. 11 | 12 | Code in `/ts` directory is for the demo app. 13 | 14 | Source code is vanilla JS. Typescipt types in `/types` directory are autogenerated with tsc. 15 | 16 | Video file is from [MixKit](https://mixkit.co/free-stock-video/a-woman-talking-on-the-phone-on-a-green-screen-24388/) 17 | 18 | ## Demo 19 | View a [live demo](https://drinkspiller.github.io/threejs_chromakey_video_material/). 20 | 21 | ## Usage/Example 22 | 23 | ```javascript 24 | import ChromaKeyMaterial from '../js/ChromaKeyMaterial'; 25 | 26 | // Assumes basic THREE.js camera/scene/renderer setup. 27 | const heightAspectRatio = 9 / 16; 28 | const geometry: THREE.BufferGeometry = 29 | new THREE.PlaneGeometry(1, heightAspectRatio); 30 | // 0x19ae31 is the green color to key out. The last three arguments are 31 | // similarity, smoothness, and spill, respectively and are used to fine 32 | // tune the color key. 33 | const greenScreenMaterial = new ChromaKeyMaterial( 34 | './assets/myVideo.mp4', 0x19ae31, 1920, 1080, 0.159, 0.082, 0.214); 35 | const plane = new THREE.Mesh(geometry, greenScreenMaterial); 36 | scene.add(plane); 37 | ``` 38 | 39 | 40 | ## Demo 41 | 42 | To run the demo in this package: 43 | 1. Clone this repo 44 | 2. `cd` into repo root 45 | 3. Install with `npm install` 46 | 4. Run demo server with `npm run dev` 47 | 5. Open http://localhost:1234 + browser Dev Tools. 48 | 6. Use dat.gui controls to pause/play video and fine tune color key. 49 | -------------------------------------------------------------------------------- /assets/mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drinkspiller/threejs_chromakey_video_material/47fcb90cddaa463c83923ea622c74ef9b33c140e/assets/mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.mp4 -------------------------------------------------------------------------------- /assets/mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drinkspiller/threejs_chromakey_video_material/47fcb90cddaa463c83923ea622c74ef9b33c140e/assets/mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.png -------------------------------------------------------------------------------- /assets/pexels-photo-1633970.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drinkspiller/threejs_chromakey_video_material/47fcb90cddaa463c83923ea622c74ef9b33c140e/assets/pexels-photo-1633970.jpg -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Directory content for serving [Demo](https://drinkspiller.github.io/threejs_chromakey_video_material/) through Github Pages 2 | -------------------------------------------------------------------------------- /docs/index.28cb5519.css: -------------------------------------------------------------------------------- 1 | *,:before,:after{box-sizing:border-box}html,body,div,span,object,iframe,figure,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,code,em,img,small,strike,strong,sub,sup,tt,b,u,i,ol,ul,li,fieldset,form,label,table,caption,tbody,tfoot,thead,tr,th,td,main,canvas,embed,footer,header,nav,section,video{font-size:100%;font:inherit;vertical-align:baseline;text-rendering:optimizelegibility;-webkit-font-smoothing:antialiased;text-size-adjust:none;border:0;margin:0;padding:0}footer,header,nav,section,main{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}input{border-radius:0}html,body{background:url(pexels-photo-1633970.fd6d4325.jpg) 0 0/cover} 2 | /*# sourceMappingURL=index.28cb5519.css.map */ 3 | -------------------------------------------------------------------------------- /docs/index.28cb5519.css.map: -------------------------------------------------------------------------------- 1 | {"mappings":"AECA,uCAIA,iaAYA,6CAIA,mBAIA,sBAIA,yBAIA,4EAKA,gDAKA,sBD1CA","sources":["index.28cb5519.css","styles/app.scss","styles/_reset.scss"],"sourcesContent":["*, :before, :after {\n box-sizing: border-box;\n}\n\nhtml, body, div, span, object, iframe, figure, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, code, em, img, small, strike, strong, sub, sup, tt, b, u, i, ol, ul, li, fieldset, form, label, table, caption, tbody, tfoot, thead, tr, th, td, main, canvas, embed, footer, header, nav, section, video {\n font-size: 100%;\n font: inherit;\n vertical-align: baseline;\n text-rendering: optimizelegibility;\n -webkit-font-smoothing: antialiased;\n text-size-adjust: none;\n border: 0;\n margin: 0;\n padding: 0;\n}\n\nfooter, header, nav, section, main {\n display: block;\n}\n\nbody {\n line-height: 1;\n}\n\nol, ul {\n list-style: none;\n}\n\nblockquote, q {\n quotes: none;\n}\n\nblockquote:before, blockquote:after, q:before, q:after {\n content: \"\";\n content: none;\n}\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ninput {\n border-radius: 0;\n}\n\nhtml, body {\n background: url(\"pexels-photo-1633970.fd6d4325.jpg\") 0 0 / cover;\n}\n\n/*# sourceMappingURL=index.28cb5519.css.map */\n","@import \"_reset.scss\";\nhtml,\nbody {\n background: url('../assets/pexels-photo-1633970.jpg');\n background-size: cover;\n}\n","// Via: https://github.com/fraserboag/sass-reset/blob/master/reset.scss\n*, *:before, *:after{\n box-sizing: border-box;\n}\n\nhtml, body, div, span, object, iframe, figure, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, code, em, img, small, strike, strong, sub, sup, tt, b, u, i, ol, ul, li, fieldset, form, label, table, caption, tbody, tfoot, thead, tr, th, td, main, canvas, embed, footer, header, nav, section, video{\n margin: 0;\n padding: 0;\n border: 0;\n font-size: 100%;\n font: inherit;\n vertical-align: baseline;\n text-rendering: optimizeLegibility;\n -webkit-font-smoothing: antialiased;\n text-size-adjust: none;\n}\n\nfooter, header, nav, section, main{\n display: block;\n}\n\nbody{\n line-height: 1;\n}\n\nol, ul{\n list-style: none;\n}\n\nblockquote, q{\n quotes: none;\n}\n\nblockquote:before, blockquote:after, q:before, q:after{\n content: '';\n content: none;\n}\n\ntable{\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ninput{\n border-radius: 0;\n}\n"],"names":[],"version":3,"file":"index.28cb5519.css.map"} -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | THREE.js ChromaKeyMaterial Demo -------------------------------------------------------------------------------- /docs/mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.a5b0f0b5.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drinkspiller/threejs_chromakey_video_material/47fcb90cddaa463c83923ea622c74ef9b33c140e/docs/mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.a5b0f0b5.mp4 -------------------------------------------------------------------------------- /docs/pexels-photo-1633970.fd6d4325.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drinkspiller/threejs_chromakey_video_material/47fcb90cddaa463c83923ea622c74ef9b33c140e/docs/pexels-photo-1633970.fd6d4325.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | THREE.js ChromaKeyMaterial Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /js/ChromaKeyMaterial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * THREE.JS ShaderMaterial that removes a specified color (e.g. greens screen) 3 | * from a texture. Shader code by https://github.com/Mugen87 on THREE.js forum: 4 | * https://discourse.threejs.org/t/production-ready-green-screen-with-three-js/23113/2 5 | */ 6 | import * as THREE from 'three'; 7 | import {ColorRepresentation} from 'three/src/utils'; 8 | 9 | import * as constants from './constants'; 10 | 11 | // eslint-disable-next-line new-cap 12 | class ChromaKeyMaterial extends THREE.ShaderMaterial { 13 | /** 14 | * 15 | * @param {string} url Image or video to load into material's texture 16 | * @param {ColorRepresentation} keyColor 17 | * @param {number} width 18 | * @param {number} height 19 | * @param {number} similarity 20 | * @param {number} smoothness 21 | * @param {number} spill 22 | */ 23 | constructor( 24 | url, keyColor, width, height, similarity = .01, smoothness = 0.18, 25 | spill = 0.1) { 26 | super(); 27 | 28 | this.url = url; 29 | this.isVideo = /\.(mp4|mov|webm)/.test(this.url); 30 | if (this.isVideo) { 31 | this.video = document.createElement('video'); 32 | this.video.src = url; 33 | this.video.muted = true; 34 | this.video.loop = true; 35 | this.video.crossorigin = 'anonymous'; 36 | this.video.autoplay = true; 37 | this.video.load(); 38 | this.video.play(); 39 | 40 | this.texture = new THREE.VideoTexture(this.video); 41 | } else { 42 | this.texture = new THREE.TextureLoader().load(url); 43 | } 44 | 45 | const chromaKeyColor = new THREE.Color(keyColor); 46 | 47 | this.setValues({ 48 | uniforms: { 49 | tex: { 50 | value: this.texture, 51 | }, 52 | keyColor: {value: chromaKeyColor}, 53 | texWidth: {value: width}, 54 | texHeight: {value: height}, 55 | similarity: {value: similarity}, 56 | smoothness: {value: smoothness}, 57 | spill: {value: spill}, 58 | 59 | }, 60 | vertexShader: constants.VERTEX_SHADER, 61 | fragmentShader: constants.FRAGMENT_SHADER, 62 | transparent: true, 63 | }); 64 | } 65 | 66 | playVideo() { 67 | if (this.isVideo && this.video) { 68 | this.video.play(); 69 | } else { 70 | throw new Error(`${this.url} is not a video file.`); 71 | } 72 | } 73 | 74 | pauseVideo() { 75 | if (this.isVideo && this.video) { 76 | this.video.pause(); 77 | } else { 78 | throw new Error(`${this.url} is not a video file.`); 79 | } 80 | } 81 | } 82 | 83 | export {ChromaKeyMaterial as default}; 84 | -------------------------------------------------------------------------------- /js/constants.js: -------------------------------------------------------------------------------- 1 | // @see https://discourse.threejs.org/t/production-ready-green-screen-with-three-js/23113/2 2 | 3 | const VERTEX_SHADER = ` 4 | varying vec2 vUv; 5 | 6 | void main() { 7 | vUv = uv; 8 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 9 | } 10 | `; 11 | const FRAGMENT_SHADER = ` 12 | uniform sampler2D tex; 13 | uniform float texWidth; 14 | uniform float texHeight; 15 | 16 | uniform vec3 keyColor; 17 | uniform float similarity; 18 | uniform float smoothness; 19 | uniform float spill; 20 | 21 | varying vec2 vUv; 22 | 23 | // From https://github.com/libretro/glsl-shaders/blob/master/nnedi3/shaders/rgb-to-yuv.glsl 24 | vec2 RGBtoUV(vec3 rgb) { 25 | return vec2( 26 | rgb.r * -0.169 + rgb.g * -0.331 + rgb.b * 0.5 + 0.5, 27 | rgb.r * 0.5 + rgb.g * -0.419 + rgb.b * -0.081 + 0.5 28 | ); 29 | } 30 | 31 | vec4 ProcessChromaKey(vec2 texCoord) { 32 | vec4 rgba = texture2D(tex, texCoord); 33 | float chromaDist = distance(RGBtoUV(texture2D(tex, texCoord).rgb), RGBtoUV(keyColor)); 34 | 35 | float baseMask = chromaDist - similarity; 36 | float fullMask = pow(clamp(baseMask / smoothness, 0., 1.), 1.5); 37 | rgba.a = fullMask; 38 | 39 | float spillVal = pow(clamp(baseMask / spill, 0., 1.), 1.5); 40 | float desat = clamp(rgba.r * 0.2126 + rgba.g * 0.7152 + rgba.b * 0.0722, 0., 1.); 41 | rgba.rgb = mix(vec3(desat, desat, desat), rgba.rgb, spillVal); 42 | 43 | return rgba; 44 | } 45 | 46 | void main(void) { 47 | vec2 texCoord = vUv; 48 | gl_FragColor = ProcessChromaKey(texCoord); 49 | } 50 | `; 51 | 52 | export { 53 | VERTEX_SHADER, 54 | FRAGMENT_SHADER, 55 | }; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "THREEjs_Chromakey_ESM Class_and_Demo", 3 | "version": "1.0.0", 4 | "description": "A THREE.JS ShaderMaterial that removes a specified color (e.g. green screen) from a video or image texture.", 5 | "browserslist": "last 2 Chrome versions, last 2 Firefox versions, last 2 Edge versions, last 2 Safari versions, last 2 Android versions, last 2 ChromeAndroid versions, last 2 iOS versions", 6 | "source": "index.html", 7 | "scripts": { 8 | "start": "npx parcel", 9 | "dev": "npm run start", 10 | "build": "npx parcel build --dist-dir docs --public-url ./" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/drinkspiller/threejs_chromakey_video_material.git" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "@parcel/transformer-sass": "^2.6.2", 20 | "@parcel/transformer-typescript-tsc": "^2.6.2", 21 | "@parcel/validator-typescript": "^2.6.2", 22 | "@types/dat.gui": "^0.7.7", 23 | "@types/three": "^0.141.0", 24 | "@typescript-eslint/parser": "^5.30.5", 25 | "eslint": "^8.19.0", 26 | "eslint-config-google": "^0.14.0", 27 | "parcel": "^2.6.2", 28 | "typescript": "^4.7.4" 29 | }, 30 | "dependencies": { 31 | "dat.gui": "^0.7.9", 32 | "rxjs": "^7.5.5", 33 | "three": "^0.142.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /styles/_reset.scss: -------------------------------------------------------------------------------- 1 | // Via: https://github.com/fraserboag/sass-reset/blob/master/reset.scss 2 | *, *:before, *:after{ 3 | box-sizing: border-box; 4 | } 5 | 6 | html, body, div, span, object, iframe, figure, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, code, em, img, small, strike, strong, sub, sup, tt, b, u, i, ol, ul, li, fieldset, form, label, table, caption, tbody, tfoot, thead, tr, th, td, main, canvas, embed, footer, header, nav, section, video{ 7 | margin: 0; 8 | padding: 0; 9 | border: 0; 10 | font-size: 100%; 11 | font: inherit; 12 | vertical-align: baseline; 13 | text-rendering: optimizeLegibility; 14 | -webkit-font-smoothing: antialiased; 15 | text-size-adjust: none; 16 | } 17 | 18 | footer, header, nav, section, main{ 19 | display: block; 20 | } 21 | 22 | body{ 23 | line-height: 1; 24 | } 25 | 26 | ol, ul{ 27 | list-style: none; 28 | } 29 | 30 | blockquote, q{ 31 | quotes: none; 32 | } 33 | 34 | blockquote:before, blockquote:after, q:before, q:after{ 35 | content: ''; 36 | content: none; 37 | } 38 | 39 | table{ 40 | border-collapse: collapse; 41 | border-spacing: 0; 42 | } 43 | 44 | input{ 45 | border-radius: 0; 46 | } 47 | -------------------------------------------------------------------------------- /styles/app.scss: -------------------------------------------------------------------------------- 1 | @import "_reset.scss"; 2 | html, 3 | body { 4 | background: url('../assets/pexels-photo-1633970.jpg'); 5 | background-size: cover; 6 | } 7 | -------------------------------------------------------------------------------- /ts/app.ts: -------------------------------------------------------------------------------- 1 | import {GUI} from 'dat.gui'; 2 | import {fromEvent} from 'rxjs'; 3 | import {debounceTime} from 'rxjs/operators'; 4 | import * as THREE from 'three'; 5 | import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'; 6 | import Stats from 'three/examples/jsm/libs/stats.module'; 7 | import videoFile from 'url:../assets/mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.mp4'; 8 | import imageFile from 'url:../assets/mixkit-a-woman-talking-on-the-phone-on-a-green-screen-24388-medium.png'; 9 | 10 | import ChromaKeyMaterial from '../js/ChromaKeyMaterial'; 11 | 12 | 13 | export class App { 14 | private ambientLight: THREE.AmbientLight = 15 | new THREE.AmbientLight(0xffffff, .3); 16 | private camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(); 17 | private plane!: THREE.Mesh; 18 | private directionalLight = new THREE.DirectionalLight(); 19 | private gui!: GUI; 20 | private renderer: THREE.WebGLRenderer = 21 | new THREE.WebGLRenderer({antialias: true, alpha: true}); 22 | private orbitControls: OrbitControls = new OrbitControls( 23 | this.camera, 24 | this.renderer.domElement, 25 | ); 26 | private queryString = new URLSearchParams(window.location.search); 27 | private scene: THREE.Scene = new THREE.Scene(); 28 | // @ts-ignore Used to avoid lint error "Only a void function can be called 29 | // with the 'new' keyword." since the types bundled with package specify it 30 | // returns type `Stats` instead of `void`. 31 | private stats: Stats = new Stats(); 32 | private static _singleton: App = new App(); 33 | 34 | static get app() { 35 | return this._singleton; 36 | } 37 | 38 | private animate() { 39 | this.render(); 40 | this.stats.update(); 41 | 42 | requestAnimationFrame(() => this.animate()); 43 | } 44 | 45 | configureCamera() { 46 | this.camera.fov = 75; 47 | this.camera.aspect = window.innerWidth / window.innerHeight; 48 | this.camera.near = 0.1; 49 | this.camera.far = 1000; 50 | this.camera.position.x = 0; 51 | this.camera.position.y = 0; 52 | this.camera.position.z = 0.67; 53 | 54 | this.camera.rotation.x = 0; 55 | this.camera.rotation.y = 0; 56 | this.camera.rotation.z = 0; 57 | 58 | this.camera.lookAt(new THREE.Vector3(0, 0, 0)); 59 | this.camera.updateProjectionMatrix(); 60 | 61 | const cameraPositionFolder = this.gui.addFolder('Camera Position'); 62 | cameraPositionFolder.add(this.camera.position, 'x', -1, 1, 0.01).listen(); 63 | cameraPositionFolder.add(this.camera.position, 'y', -1, 1, 0.01).listen(); 64 | cameraPositionFolder.add(this.camera.position, 'z', -1, 1, 0.01).listen(); 65 | cameraPositionFolder.open(); 66 | 67 | const cameraRotationFolder = this.gui.addFolder('Camera Rotation'); 68 | cameraRotationFolder.add(this.camera.rotation, 'x', -50, 50, 0.01).listen(); 69 | cameraRotationFolder.add(this.camera.rotation, 'y', -50, 50, 0.01).listen(); 70 | cameraRotationFolder.add(this.camera.rotation, 'z', -50, 50, 0.01).listen(); 71 | cameraRotationFolder.open(); 72 | } 73 | 74 | configureDev() { 75 | this.gui = new GUI(); 76 | document.body.appendChild(this.stats.dom); 77 | 78 | const gridHelper = new THREE.GridHelper(5, undefined, 'yellow', 'gray'); 79 | // this.scene.add(gridHelper); 80 | 81 | const orbitControlsFolder = this.gui.addFolder('Orbit Controls'); 82 | const orbitControlOptions = { 83 | enabled: true, 84 | }; 85 | this.orbitControls.enabled = orbitControlOptions.enabled; 86 | orbitControlsFolder.add(orbitControlOptions, 'enabled') 87 | .onChange((isEnabled) => { 88 | this.orbitControls.enabled = isEnabled; 89 | }); 90 | orbitControlsFolder.open(); 91 | 92 | const functionsObject = { 93 | playVideo: () => { 94 | (this.plane.material as ChromaKeyMaterial).playVideo(); 95 | }, 96 | pauseVideo: () => { 97 | (this.plane.material as ChromaKeyMaterial).pauseVideo(); 98 | }, 99 | }; 100 | const functionsFolder = this.gui.addFolder('Functions'); 101 | functionsFolder.add(functionsObject, 'playVideo'); 102 | functionsFolder.add(functionsObject, 'pauseVideo'); 103 | functionsFolder.open(); 104 | } 105 | 106 | configureEventListeners() { 107 | fromEvent(window, 'resize') 108 | .pipe(debounceTime(75)) 109 | .subscribe(() => this.updateResizedWindow()); 110 | } 111 | 112 | configureLights() { 113 | this.directionalLight.color = new THREE.Color(0xffffff); 114 | this.directionalLight.intensity = 1; 115 | this.directionalLight.position.set(2, 2, 2); 116 | 117 | this.scene.add(this.directionalLight, this.ambientLight); 118 | 119 | const directionalLightHelper = new THREE.DirectionalLightHelper( 120 | this.directionalLight, 121 | 1, 122 | ); 123 | // this.scene.add(directionalLightHelper); 124 | 125 | const directionalLightFolder = this.gui.addFolder('Directional Light'); 126 | directionalLightFolder.add(this.directionalLight.position, 'x', 0, 50, 0.01) 127 | .listen(); 128 | directionalLightFolder.add(this.directionalLight.position, 'y', 0, 50, 0.01) 129 | .listen(); 130 | directionalLightFolder.add(this.directionalLight.position, 'z', 0, 50, 0.01) 131 | .listen(); 132 | directionalLightFolder.add(this.directionalLight, 'intensity', 0, 5, 0.01) 133 | .listen(); 134 | directionalLightFolder.open(); 135 | } 136 | 137 | configureMesh() { 138 | const heightAspectRatio = 9 / 16; 139 | const geometry: THREE.BufferGeometry = 140 | new THREE.PlaneGeometry(1, heightAspectRatio); 141 | 142 | // Use `imageFile` instead of `videoFile` to key a static image instead of a 143 | // video. 144 | const greenScreenMaterial = new ChromaKeyMaterial( 145 | videoFile, 0x19ae31, 1920, 1080, 0.159, 0.082, 0.214); 146 | this.plane = new THREE.Mesh(geometry, greenScreenMaterial); 147 | this.plane.position.y = -.2; 148 | this.scene.add(this.plane); 149 | 150 | const wireframe = new THREE.WireframeGeometry(geometry); 151 | const line = new THREE.LineSegments( 152 | wireframe, new THREE.LineBasicMaterial({color: 0xff0000})); 153 | this.plane.add(line); 154 | 155 | const chromakeyMaterialFolder = this.gui.addFolder('Chroma Key'); 156 | chromakeyMaterialFolder 157 | .add(greenScreenMaterial.uniforms.similarity, 'value', 0, 1, .001) 158 | .name('Similarity'); 159 | chromakeyMaterialFolder 160 | .add(greenScreenMaterial.uniforms.smoothness, 'value', 0, 1, .001) 161 | .name('Smoothness'); 162 | chromakeyMaterialFolder 163 | .add(greenScreenMaterial.uniforms.spill, 'value', 0, 1, .001) 164 | .name('Spill'); 165 | chromakeyMaterialFolder.open(); 166 | } 167 | 168 | configureRenderer() { 169 | this.renderer.setClearColor(0x000000, 0); 170 | this.renderer.setSize(window.innerWidth, window.innerHeight); 171 | document.body.appendChild(this.renderer.domElement); 172 | } 173 | 174 | init() { 175 | this.configureDev(); 176 | this.configureCamera(); 177 | this.configureMesh(); 178 | this.configureLights(); 179 | this.configureRenderer(); 180 | this.configureEventListeners(); 181 | 182 | this.animate(); 183 | } 184 | 185 | render() { 186 | this.renderer.render(this.scene, this.camera); 187 | } 188 | 189 | updateResizedWindow() { 190 | this.camera.aspect = window.innerWidth / window.innerHeight; 191 | this.camera.updateProjectionMatrix(); 192 | this.renderer.setSize(window.innerWidth, window.innerHeight); 193 | this.render(); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /ts/index.ts: -------------------------------------------------------------------------------- 1 | import {App} from './app'; 2 | 3 | const app = App.app; 4 | app.init(); 5 | -------------------------------------------------------------------------------- /ts/parcel.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.jpg'; 3 | declare module '*.gif'; 4 | declare module 'url:*'; 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "jsx": "preserve", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "target": "es6", 10 | "lib": [ 11 | "es6", 12 | "dom" 13 | ], 14 | "moduleResolution": "node" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /types/ChromaKeyMaterial.d.ts: -------------------------------------------------------------------------------- 1 | export {ChromaKeyMaterial as default}; 2 | declare class ChromaKeyMaterial extends THREE.ShaderMaterial { 3 | /** 4 | * 5 | * @param {string} url Image or video to load into material's texture 6 | * @param {ColorRepresentation} keyColor 7 | * @param {number} width 8 | * @param {number} height 9 | * @param {number} similarity 10 | * @param {number} smoothness 11 | * @param {number} spill 12 | */ 13 | constructor( 14 | url: string, keyColor: ColorRepresentation, width: number, height: number, 15 | similarity?: number, smoothness?: number, spill?: number); 16 | url: string; 17 | isVideo: boolean; 18 | video: HTMLVideoElement; 19 | texture: THREE.Texture|THREE.VideoTexture; 20 | playVideo(): void; 21 | pauseVideo(): void; 22 | } 23 | import * as THREE from 'three'; 24 | import {ColorRepresentation} from 'three/src/utils'; 25 | // # sourceMappingURL=ChromaKeyMaterial.d.ts.map 26 | -------------------------------------------------------------------------------- /types/ChromaKeyMaterial.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ChromaKeyMaterial.d.ts","sourceRoot":"","sources":["../js/ChromaKeyMaterial.js"],"names":[],"mappings":";AAWA;IACE;;;;;;;;;OASG;IACH,iBARW,MAAM,YACN,mBAAmB,SACnB,MAAM,UACN,MAAM,eACN,MAAM,eACN,MAAM,UACN,MAAM,EA2ChB;IApCC,YAAc;IACd,iBAAgD;IAE9C,wBAA4C;IAS5C,4CAAiD;IA0BrD,kBAMC;IAED,mBAMC;CACF"} -------------------------------------------------------------------------------- /types/constants.d.ts: -------------------------------------------------------------------------------- 1 | export const VERTEX_SHADER: 2 | '\nvarying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}\n'; 3 | export const FRAGMENT_SHADER: 4 | '\nuniform sampler2D tex;\nuniform float texWidth;\nuniform float texHeight;\n\nuniform vec3 keyColor;\nuniform float similarity;\nuniform float smoothness;\nuniform float spill;\n\nvarying vec2 vUv;\n\n// From https://github.com/libretro/glsl-shaders/blob/master/nnedi3/shaders/rgb-to-yuv.glsl\nvec2 RGBtoUV(vec3 rgb) {\n return vec2(\n rgb.r * -0.169 + rgb.g * -0.331 + rgb.b * 0.5 + 0.5,\n rgb.r * 0.5 + rgb.g * -0.419 + rgb.b * -0.081 + 0.5\n );\n}\n\nvec4 ProcessChromaKey(vec2 texCoord) {\n vec4 rgba = texture2D(tex, texCoord);\n float chromaDist = distance(RGBtoUV(texture2D(tex, texCoord).rgb), RGBtoUV(keyColor));\n\n float baseMask = chromaDist - similarity;\n float fullMask = pow(clamp(baseMask / smoothness, 0., 1.), 1.5);\n rgba.a = fullMask;\n\n float spillVal = pow(clamp(baseMask / spill, 0., 1.), 1.5);\n float desat = clamp(rgba.r * 0.2126 + rgba.g * 0.7152 + rgba.b * 0.0722, 0., 1.);\n rgba.rgb = mix(vec3(desat, desat, desat), rgba.rgb, spillVal);\n\n return rgba;\n}\n\nvoid main(void) {\n vec2 texCoord = vUv;\n gl_FragColor = ProcessChromaKey(texCoord);\n}\n'; 5 | // # sourceMappingURL=constants.d.ts.map 6 | -------------------------------------------------------------------------------- /types/constants.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../js/constants.js"],"names":[],"mappings":"AAEA,kKAOE;AACF,unCAuCE"} --------------------------------------------------------------------------------