├── .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"}
--------------------------------------------------------------------------------