├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── 3dof ├── .yarnrc.yml ├── .gitignore ├── rollup.config.js ├── LICENSE ├── package.json ├── README.md ├── src │ └── index.js └── example │ └── index.html ├── fix-fog ├── .yarnrc.yml ├── .gitignore ├── rollup.config.js ├── src │ └── index.js ├── LICENSE ├── package.json ├── README.md └── example │ └── index.html ├── hud ├── .yarnrc.yml ├── .gitignore ├── rollup.config.js ├── LICENSE ├── package.json ├── README.md ├── example │ └── index.html └── src │ └── index.js ├── mirror ├── .yarnrc.yml ├── .gitignore ├── rollup.config.js ├── LICENSE ├── package.json ├── README.md ├── example │ └── index.html └── src │ └── index.js ├── effekseer ├── .yarnrc.yml ├── .gitignore ├── src │ ├── main.ts │ ├── effekseer.component.ts │ └── effekseer.system.ts ├── rollup.config.dev.js ├── tsconfig.json ├── rollup.config.prod.js ├── package.json ├── vendor │ ├── README.md │ └── effekseer.d.ts ├── README.md └── example │ └── index.html ├── extra-stats ├── .yarnrc.yml ├── .gitignore ├── src │ ├── rStats.three.js │ ├── index.js │ └── rStats.three-alloc.js ├── rollup.config.js ├── LICENSE ├── package.json ├── example │ └── index.html └── README.md ├── highlight ├── .yarnrc.yml ├── .gitignore ├── rollup.config.js ├── LICENSE ├── package.json ├── example │ └── index.html ├── README.md └── src │ └── index.js ├── screen-fade ├── .yarnrc.yml ├── .gitignore ├── rollup.config.js ├── tsconfig.json ├── types │ └── screen-fade.d.ts ├── LICENSE ├── example │ └── index.html ├── src │ └── index.js ├── package.json └── README.md ├── motion-controller ├── .yarnrc.yml ├── .gitignore ├── src │ ├── main.ts │ ├── hand-joint-names.ts │ ├── motion-controller-space.component.ts │ ├── utils.ts │ ├── motion-controller.system.ts │ └── motion-controller-model.component.ts ├── rollup.config.dev.js ├── tsconfig.json ├── README.md ├── rollup.config.prod.js ├── package.json └── example │ └── index.html ├── sky-background ├── .yarnrc.yml ├── .gitignore ├── rollup.config.js ├── tsconfig.json ├── LICENSE ├── example │ └── index.html ├── package.json ├── types │ └── sky-background.d.ts ├── README.md └── src │ └── index.js ├── .yarnrc.yml ├── package.json ├── scripts ├── README.md ├── build-gh-pages.sh └── chunks │ ├── github-corner-right.html │ └── github-corner-left.html └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: fernsolutions -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ -------------------------------------------------------------------------------- /3dof/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /fix-fog/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /hud/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /mirror/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /effekseer/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /extra-stats/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /highlight/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /screen-fade/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /highlight/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ -------------------------------------------------------------------------------- /hud/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ 4 | site/ -------------------------------------------------------------------------------- /motion-controller/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /sky-background/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /3dof/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ 4 | site/ -------------------------------------------------------------------------------- /fix-fog/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ 4 | site/ -------------------------------------------------------------------------------- /mirror/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ 4 | site/ -------------------------------------------------------------------------------- /effekseer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ 4 | temp/ -------------------------------------------------------------------------------- /extra-stats/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ 4 | site/ -------------------------------------------------------------------------------- /screen-fade/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ 4 | site/ -------------------------------------------------------------------------------- /motion-controller/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ 4 | temp/ -------------------------------------------------------------------------------- /sky-background/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ 4 | site/ -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | -------------------------------------------------------------------------------- /effekseer/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'aframe'; 2 | import './effekseer.component'; 3 | import './effekseer.system'; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fern-aframe-components", 3 | "workspaces": [ 4 | "*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /motion-controller/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'aframe'; 2 | import './motion-controller-model.component'; 3 | import './motion-controller-space.component'; 4 | import './motion-controller.system'; -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | Some simple scripts to populate the `dist/` folder with built versions of all the components and their corresponding examples, ready to be deployed to GitHub Pages. See the `.github/workflows/ci.yml` file for the entire process. 2 | -------------------------------------------------------------------------------- /extra-stats/src/rStats.three.js: -------------------------------------------------------------------------------- 1 | export function updatedThreeStats(renderer) { 2 | const rawThreeStats = window.threeStats(renderer); 3 | 4 | // Sane limits for mobile (Quest 2, Pico Neo 3 Link, etc..) HMDs 5 | rawThreeStats.values['renderer.info.render.calls'].over = 150; 6 | rawThreeStats.values['renderer.info.render.triangles'].over = 1_200_000; 7 | 8 | return rawThreeStats; 9 | } -------------------------------------------------------------------------------- /3dof/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from "rollup-plugin-terser"; 2 | import pkg from './package.json'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | plugins: [ 7 | terser(), 8 | ], 9 | output: [ 10 | { 11 | file: pkg.browser, 12 | format: 'umd', 13 | }, 14 | { 15 | file: pkg.module, 16 | format: 'es' 17 | }, 18 | ], 19 | }; -------------------------------------------------------------------------------- /hud/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from "rollup-plugin-terser"; 2 | import pkg from './package.json'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | plugins: [ 7 | terser(), 8 | ], 9 | output: [ 10 | { 11 | file: pkg.browser, 12 | format: 'umd', 13 | }, 14 | { 15 | file: pkg.module, 16 | format: 'es' 17 | }, 18 | ], 19 | }; -------------------------------------------------------------------------------- /mirror/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from "rollup-plugin-terser"; 2 | import pkg from './package.json'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | plugins: [ 7 | terser(), 8 | ], 9 | output: [ 10 | { 11 | file: pkg.browser, 12 | format: 'umd', 13 | }, 14 | { 15 | file: pkg.module, 16 | format: 'es' 17 | }, 18 | ], 19 | }; -------------------------------------------------------------------------------- /extra-stats/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from "rollup-plugin-terser"; 2 | import pkg from './package.json'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | plugins: [ 7 | terser(), 8 | ], 9 | output: [ 10 | { 11 | file: pkg.browser, 12 | format: 'umd', 13 | }, 14 | { 15 | file: pkg.module, 16 | format: 'es' 17 | }, 18 | ], 19 | }; -------------------------------------------------------------------------------- /fix-fog/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from "rollup-plugin-terser"; 2 | import pkg from './package.json'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | plugins: [ 7 | terser(), 8 | ], 9 | output: [ 10 | { 11 | file: pkg.browser, 12 | format: 'umd', 13 | }, 14 | { 15 | file: pkg.module, 16 | format: 'es' 17 | }, 18 | ], 19 | }; -------------------------------------------------------------------------------- /highlight/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from "rollup-plugin-terser"; 2 | import pkg from './package.json'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | plugins: [ 7 | terser(), 8 | ], 9 | output: [ 10 | { 11 | file: pkg.browser, 12 | format: 'umd', 13 | }, 14 | { 15 | file: pkg.module, 16 | format: 'es' 17 | }, 18 | ], 19 | }; -------------------------------------------------------------------------------- /screen-fade/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from "rollup-plugin-terser"; 2 | import pkg from './package.json'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | plugins: [ 7 | terser(), 8 | ], 9 | output: [ 10 | { 11 | file: pkg.browser, 12 | format: 'umd', 13 | }, 14 | { 15 | file: pkg.module, 16 | format: 'es' 17 | }, 18 | ], 19 | }; -------------------------------------------------------------------------------- /sky-background/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from "rollup-plugin-terser"; 2 | import pkg from './package.json'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | plugins: [ 7 | terser(), 8 | ], 9 | output: [ 10 | { 11 | file: pkg.browser, 12 | format: 'umd', 13 | }, 14 | { 15 | file: pkg.module, 16 | format: 'es' 17 | }, 18 | ], 19 | }; -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: 7 | contents: write 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - run: yarn set version stable 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: 3.x 17 | - run: pip install ghp-import 18 | - name: install, build, and test 19 | run: | 20 | ./scripts/build-gh-pages.sh 21 | - name: deploy 22 | run: ghp-import ./dist -c aframe-components.fern.solutions -p -f 23 | -------------------------------------------------------------------------------- /screen-fade/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noImplicitAny": true, 17 | "skipLibCheck": true, 18 | "declaration": false, 19 | "paths": { 20 | "aframe": ["./node_modules/aframe-types"] 21 | } 22 | }, 23 | "include": ["types"] 24 | } 25 | -------------------------------------------------------------------------------- /sky-background/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noImplicitAny": true, 17 | "skipLibCheck": true, 18 | "declaration": false, 19 | "paths": { 20 | "aframe": ["./node_modules/aframe-types"] 21 | } 22 | }, 23 | "include": ["types"] 24 | } 25 | -------------------------------------------------------------------------------- /screen-fade/types/screen-fade.d.ts: -------------------------------------------------------------------------------- 1 | import "aframe"; 2 | 3 | declare module "aframe" { 4 | import { Component } from "aframe"; 5 | 6 | export interface Components { 7 | /** 8 | * Component allowing the screen to be faded to and from a solid color. 9 | * The effect works in both desktop and VR mode. This can be used for situations like 10 | * loading a scene, handling transitions or snap turning. 11 | */ 12 | "screen-fade": Component<{ 13 | /** The solid color the screen fades to */ 14 | 'color': { type: "color", default: "#000000" }, 15 | /** The intensity of the fade between 0.0 and 1.0 */ 16 | 'intensity': { type: "number", default: 0.0, max: 1.0, min: 0.0 } 17 | }> 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/build-gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | yarn workspaces foreach -A install --immutable 5 | yarn workspaces foreach -A run build 6 | 7 | rm -rf ./dist 8 | mkdir -p ./dist/js/ 9 | cp README.md ./dist/ 10 | for dir in ./*/ 11 | do 12 | dir=$(basename $dir) 13 | if [ -e "./$dir/package.json" ]; then 14 | mkdir -p "./dist/$dir/" 15 | cp -R "./$dir/dist/"*".umd.min.js" ./dist/js/ 16 | cp -R "./$dir/example/"* "./dist/$dir/" 17 | fi 18 | done 19 | 20 | # Insert GitHub corner into example files 21 | find "./dist/" -name "*.html" -exec sed -i ' 22 | //{ 23 | r ./scripts/chunks/github-corner-left.html 24 | D 25 | } 26 | //{ 27 | r ./scripts/chunks/github-corner-right.html 28 | D 29 | }' {} \; 30 | -------------------------------------------------------------------------------- /motion-controller/rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'rollup-plugin-esbuild'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import pkg from './package.json'; 4 | 5 | export default [ 6 | { 7 | input: 'src/main.ts', 8 | plugins: [ 9 | nodeResolve({ resolveOnly: ['@webxr-input-profiles/motion-controllers'] }), 10 | esbuild(), 11 | ], 12 | external: ['aframe'], 13 | output: [ 14 | { 15 | name: 'aframe-motion-controller', 16 | file: pkg.browser, 17 | sourcemap: true, 18 | format: 'umd', 19 | globals: { 20 | aframe: 'AFRAME', 21 | three: 'THREE' 22 | } 23 | } 24 | ], 25 | } 26 | ] -------------------------------------------------------------------------------- /effekseer/rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import pkg from './package.json'; 4 | 5 | export default [ 6 | { 7 | input: 'src/main.ts', 8 | plugins: [ 9 | nodeResolve(), 10 | typescript({ sourceMap: true }), 11 | ], 12 | external: ['aframe', 'effekseer'], 13 | output: [ 14 | { 15 | name: 'aframe-effekseer', 16 | file: pkg.browser, 17 | sourcemap: true, 18 | format: 'umd', 19 | globals: { 20 | aframe: 'AFRAME', 21 | three: 'THREE', 22 | "@zip.js/zip.js": "zip" 23 | } 24 | } 25 | ], 26 | } 27 | ] -------------------------------------------------------------------------------- /motion-controller/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "baseUrl": "./", 7 | "lib": ["esnext", "dom"], 8 | "moduleResolution": "node", 9 | "strict": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noImplicitAny": true, 18 | "skipLibCheck": true, 19 | "declaration": false, 20 | "rootDir": "src", 21 | "paths": { 22 | "aframe": ["../node_modules/aframe-types"] 23 | } 24 | }, 25 | "include": ["src"], 26 | "exclude": ["node_modules", "**/dist/*"] 27 | } 28 | -------------------------------------------------------------------------------- /effekseer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "baseUrl": "./", 7 | "lib": ["esnext", "dom"], 8 | "moduleResolution": "node", 9 | "strict": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noImplicitAny": true, 18 | "skipLibCheck": true, 19 | "declaration": false, 20 | "rootDir": "src", 21 | "paths": { 22 | "aframe": ["../node_modules/aframe-types"], 23 | "effekseer": ["vendor/effekseer.d.ts"] 24 | } 25 | }, 26 | "include": ["src"], 27 | "exclude": ["node_modules", "**/dist/*"] 28 | } 29 | -------------------------------------------------------------------------------- /fix-fog/src/index.js: -------------------------------------------------------------------------------- 1 | THREE.ShaderChunk.fog_pars_vertex = /*glsl*/` 2 | #ifdef USE_FOG 3 | varying vec3 vFogPosition; 4 | #endif`; 5 | 6 | THREE.ShaderChunk.fog_vertex = /*glsl*/` 7 | #ifdef USE_FOG 8 | vFogPosition = worldPosition.xyz; 9 | #endif`; 10 | 11 | THREE.ShaderChunk.fog_pars_fragment = /*glsl*/` 12 | #ifdef USE_FOG 13 | uniform vec3 fogColor; 14 | varying vec3 vFogPosition; 15 | #ifdef FOG_EXP2 16 | uniform float fogDensity; 17 | #else 18 | uniform float fogNear; 19 | uniform float fogFar; 20 | #endif 21 | #endif`; 22 | 23 | THREE.ShaderChunk.fog_fragment = /*glsl*/` 24 | #ifdef USE_FOG 25 | float fogDepth = distance( cameraPosition, vFogPosition ); 26 | #ifdef FOG_EXP2 27 | float fogFactor = 1.0 - exp( - fogDensity * fogDensity * fogDepth * fogDepth ); 28 | #else 29 | float fogFactor = smoothstep( fogNear, fogFar, fogDepth ); 30 | #endif 31 | gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor ); 32 | #endif`; -------------------------------------------------------------------------------- /motion-controller/src/hand-joint-names.ts: -------------------------------------------------------------------------------- 1 | export const HAND_JOINT_NAMES = [ 2 | "wrist", 3 | 4 | "thumb-metacarpal", 5 | "thumb-phalanx-proximal", 6 | "thumb-phalanx-distal", 7 | "thumb-tip", 8 | 9 | "index-finger-metacarpal", 10 | "index-finger-phalanx-proximal", 11 | "index-finger-phalanx-intermediate", 12 | "index-finger-phalanx-distal", 13 | "index-finger-tip", 14 | 15 | "middle-finger-metacarpal", 16 | "middle-finger-phalanx-proximal", 17 | "middle-finger-phalanx-intermediate", 18 | "middle-finger-phalanx-distal", 19 | "middle-finger-tip", 20 | 21 | "ring-finger-metacarpal", 22 | "ring-finger-phalanx-proximal", 23 | "ring-finger-phalanx-intermediate", 24 | "ring-finger-phalanx-distal", 25 | "ring-finger-tip", 26 | 27 | "pinky-finger-metacarpal", 28 | "pinky-finger-phalanx-proximal", 29 | "pinky-finger-phalanx-intermediate", 30 | "pinky-finger-phalanx-distal", 31 | "pinky-finger-tip" 32 | ]; 33 | -------------------------------------------------------------------------------- /hud/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Noeri Huisman 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 | -------------------------------------------------------------------------------- /3dof/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2024 Noeri Huisman 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 | -------------------------------------------------------------------------------- /fix-fog/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Noeri Huisman 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 | -------------------------------------------------------------------------------- /highlight/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Noeri Huisman 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 | -------------------------------------------------------------------------------- /mirror/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Noeri Huisman 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 | -------------------------------------------------------------------------------- /extra-stats/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Noeri Huisman 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 | -------------------------------------------------------------------------------- /screen-fade/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Noeri Huisman 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 | -------------------------------------------------------------------------------- /sky-background/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Noeri Huisman 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 | -------------------------------------------------------------------------------- /3dof/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fern-solutions/aframe-3dof", 3 | "version": "1.1.1", 4 | "description": "A-Frame component for rendering in 3DoF when in VR", 5 | "module": "dist/3dof.esm.min.js", 6 | "browser": "dist/3dof.umd.min.js", 7 | "main": "dist/3dof.esm.min.js", 8 | "author": "Noeri Huisman", 9 | "license": "MIT", 10 | "scripts": { 11 | "dev": "concurrently \"rollup -c -w\" \"live-server --port=4000 --no-browser ./example --mount=/js:./dist\"", 12 | "build": "rollup -c", 13 | "watch": "rollup -c -w" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mrxz/fern-aframe-components" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/mrxz/fern-aframe-components/issues" 21 | }, 22 | "homepage": "https://github.com/mrxz/fern-aframe-components/tree/main/3dof#readme", 23 | "keywords": [ 24 | "aframe", 25 | "vr", 26 | "xr", 27 | "webxr" 28 | ], 29 | "files": [ 30 | "dist" 31 | ], 32 | "devDependencies": { 33 | "@compodoc/live-server": "1.2.3", 34 | "concurrently": "^7.1.0", 35 | "rollup": "^2.71.1", 36 | "rollup-plugin-terser": "^7.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mirror/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fern-solutions/aframe-mirror", 3 | "version": "1.1.1", 4 | "description": "A-Frame component for high-quality mirrors", 5 | "module": "dist/mirror.esm.min.js", 6 | "browser": "dist/mirror.umd.min.js", 7 | "main": "dist/mirror.esm.min.js", 8 | "author": "Noeri Huisman", 9 | "license": "MIT", 10 | "scripts": { 11 | "dev": "concurrently \"rollup -c -w\" \"live-server --port=4000 --no-browser ./example --mount=/js:./dist\"", 12 | "build": "rollup -c", 13 | "watch": "rollup -c -w" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mrxz/fern-aframe-components" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/mrxz/fern-aframe-components/issues" 21 | }, 22 | "homepage": "https://github.com/mrxz/fern-aframe-components/tree/main/mirror/#readme", 23 | "keywords": [ 24 | "aframe", 25 | "vr", 26 | "xr", 27 | "webxr" 28 | ], 29 | "files": [ 30 | "dist" 31 | ], 32 | "devDependencies": { 33 | "@compodoc/live-server": "1.2.3", 34 | "concurrently": "^7.1.0", 35 | "rollup": "^2.71.1", 36 | "rollup-plugin-terser": "^7.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /fix-fog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fern-solutions/aframe-fix-fog", 3 | "version": "1.0.0", 4 | "description": "Improves fog in VR by computing depth in world space", 5 | "module": "dist/fix-fog.esm.min.js", 6 | "browser": "dist/fix-fog.umd.min.js", 7 | "main": "dist/fix-fog.esm.min.js", 8 | "author": "Noeri Huisman", 9 | "license": "MIT", 10 | "scripts": { 11 | "dev": "concurrently \"rollup -c -w\" \"live-server --port=4000 --no-browser ./example --mount=/js:./dist\"", 12 | "build": "rollup -c", 13 | "watch": "rollup -c -w" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mrxz/fern-aframe-components" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/mrxz/fern-aframe-components/issues" 21 | }, 22 | "homepage": "https://github.com/mrxz/fern-aframe-components/tree/main/fix-fog/#readme", 23 | "keywords": [ 24 | "aframe", 25 | "vr", 26 | "xr", 27 | "webxr" 28 | ], 29 | "files": [ 30 | "dist" 31 | ], 32 | "devDependencies": { 33 | "@compodoc/live-server": "1.2.3", 34 | "concurrently": "^7.1.0", 35 | "rollup": "^2.71.1", 36 | "rollup-plugin-terser": "^7.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /hud/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fern-solutions/aframe-hud", 3 | "version": "0.1.0", 4 | "description": "A-Frame component for adding HUD elements on top of the rest in both desktop and VR", 5 | "module": "dist/hud.esm.min.js", 6 | "browser": "dist/hud.umd.min.js", 7 | "main": "dist/hud.esm.min.js", 8 | "author": "Noeri Huisman", 9 | "license": "MIT", 10 | "scripts": { 11 | "dev": "concurrently \"rollup -c -w\" \"live-server --port=4000 --no-browser ./example --mount=/js:./dist\"", 12 | "build": "rollup -c", 13 | "watch": "rollup -c -w" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mrxz/fern-aframe-components" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/mrxz/fern-aframe-components/issues" 21 | }, 22 | "homepage": "https://github.com/mrxz/fern-aframe-components/tree/main/hud#readme", 23 | "keywords": [ 24 | "aframe", 25 | "vr", 26 | "xr", 27 | "webxr" 28 | ], 29 | "files": [ 30 | "dist" 31 | ], 32 | "devDependencies": { 33 | "@compodoc/live-server": "1.2.3", 34 | "concurrently": "^7.1.0", 35 | "rollup": "^2.71.1", 36 | "rollup-plugin-terser": "^7.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /highlight/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fern-solutions/aframe-highlight", 3 | "version": "0.2.0", 4 | "description": "A-Frame component for giving objects a highlight when occluded", 5 | "module": "dist/highlight.esm.min.js", 6 | "browser": "dist/highlight.umd.min.js", 7 | "main": "dist/highlight.esm.min.js", 8 | "author": "Noeri Huisman", 9 | "license": "MIT", 10 | "scripts": { 11 | "dev": "concurrently \"rollup -c -w\" \"live-server --port=4000 --no-browser ./example --mount=/js:./dist\"", 12 | "build": "rollup -c", 13 | "watch": "rollup -c -w" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mrxz/fern-aframe-components" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/mrxz/fern-aframe-components/issues" 21 | }, 22 | "homepage": "https://github.com/mrxz/fern-aframe-components/tree/main/highlight/#readme", 23 | "keywords": [ 24 | "aframe", 25 | "vr", 26 | "xr", 27 | "webxr" 28 | ], 29 | "files": [ 30 | "dist" 31 | ], 32 | "devDependencies": { 33 | "@compodoc/live-server": "1.2.3", 34 | "concurrently": "^7.1.0", 35 | "rollup": "^2.71.1", 36 | "rollup-plugin-terser": "^7.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sky-background/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fern A-Frame Components | Screen Fade 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /extra-stats/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fern-solutions/aframe-extra-stats", 3 | "version": "1.0.2", 4 | "description": "A-Frame component that adds additional stats to the built-in stats component", 5 | "module": "dist/extra-stats.esm.min.js", 6 | "browser": "dist/extra-stats.umd.min.js", 7 | "main": "dist/extra-stats.esm.min.js", 8 | "author": "Noeri Huisman", 9 | "license": "MIT", 10 | "scripts": { 11 | "dev": "concurrently \"rollup -c -w\" \"live-server --port=4000 --no-browser ./example --mount=/js:./dist\"", 12 | "build": "rollup -c", 13 | "watch": "rollup -c -w" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mrxz/fern-aframe-components" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/mrxz/fern-aframe-components/issues" 21 | }, 22 | "homepage": "https://github.com/mrxz/fern-aframe-components/tree/main/extra-stats/#readme", 23 | "keywords": [ 24 | "aframe", 25 | "vr", 26 | "xr", 27 | "webxr" 28 | ], 29 | "files": [ 30 | "dist" 31 | ], 32 | "devDependencies": { 33 | "@compodoc/live-server": "1.2.3", 34 | "concurrently": "^7.1.0", 35 | "rollup": "^2.71.1", 36 | "rollup-plugin-terser": "^7.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /motion-controller/README.md: -------------------------------------------------------------------------------- 1 | # Motion Controller component 2 | [![npm version](https://img.shields.io/npm/v/@fern-solutions/aframe-motion-controller.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-motion-controller) 3 | [![npm version](https://img.shields.io/npm/l/@fern-solutions/aframe-motion-controller.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-motion-controller) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 6 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 7 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 8 | 9 | https://github.com/immersive-web/webxr-input-profiles 10 | 11 | Checkout the example: [Online Demo](https://aframe-components.fern.solutions/motion-controller) | [Source](https://github.com/mrxz/fern-aframe-components/blob/main/motion-controller/example/index.html) 12 | 13 | ## Usage 14 | 15 | 16 | ## Properties 17 | -------------------------------------------------------------------------------- /motion-controller/rollup.config.prod.js: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import dts from 'rollup-plugin-dts'; 4 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 5 | import pkg from './package.json'; 6 | 7 | export default [ 8 | { 9 | input: 'src/main.ts', 10 | plugins: [ 11 | nodeResolve({ resolveOnly: ['aframe-typescript'] }), 12 | typescript({ compilerOptions: { declaration: true, declarationDir: 'typings' } }), 13 | terser(), 14 | ], 15 | external: ['aframe'], 16 | output: [ 17 | { 18 | name: 'aframe-motion-controller', 19 | file: pkg.browser, 20 | format: 'umd', 21 | globals: { 22 | aframe: 'AFRAME', 23 | three: 'THREE' 24 | } 25 | }, 26 | { 27 | file: pkg.module, 28 | format: 'es' 29 | }, 30 | ], 31 | }, 32 | { 33 | input: './dist/typings/main.d.ts', 34 | output: [{ file: 'dist/aframe-motion-controller.d.ts', format: "es" }], 35 | plugins: [dts()], 36 | external: ['aframe'], 37 | } 38 | ] -------------------------------------------------------------------------------- /effekseer/rollup.config.prod.js: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import dts from 'rollup-plugin-dts'; 4 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 5 | import pkg from './package.json'; 6 | 7 | export default [ 8 | { 9 | input: 'src/main.ts', 10 | plugins: [ 11 | nodeResolve(), 12 | typescript({ compilerOptions: { declaration: true, declarationDir: 'typings' } }), 13 | terser(), 14 | ], 15 | external: ['aframe', 'effekseer'], 16 | output: [ 17 | { 18 | name: 'aframe-effekseer', 19 | file: pkg.browser, 20 | format: 'umd', 21 | globals: { 22 | aframe: 'AFRAME', 23 | three: 'THREE', 24 | "@zip.js/zip.js": "zip" 25 | } 26 | }, 27 | { 28 | file: pkg.module, 29 | format: 'es' 30 | }, 31 | ], 32 | }, 33 | { 34 | input: './dist/typings/main.d.ts', 35 | output: [{ file: 'dist/aframe-effekseer.d.ts', format: "es" }], 36 | plugins: [dts()], 37 | external: ['aframe', 'effekseer'], 38 | } 39 | ] -------------------------------------------------------------------------------- /screen-fade/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fern A-Frame Components | Screen Fade 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /effekseer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fern-solutions/aframe-effekseer", 3 | "version": "1.0.0", 4 | "description": "A-Frame component for rendering Effekseer effects", 5 | "module": "dist/aframe-effekseer.esm.min.js", 6 | "browser": "dist/aframe-effekseer.umd.min.js", 7 | "main": "dist/aframe-effekseer.esm.min.js", 8 | "types": "dist/aframe-effekseer.d.ts", 9 | "author": "Noeri Huisman", 10 | "license": "MIT", 11 | "scripts": { 12 | "dev": "concurrently \"rollup -c rollup.config.dev.js -w\" \"live-server --port=4000 --no-browser ./example --mount=/js:./dist --mount=/src:./src\"", 13 | "build": "rollup -c rollup.config.prod.js" 14 | }, 15 | "keywords": [ 16 | "aframe", 17 | "typescript", 18 | "webxr" 19 | ], 20 | "files": [ 21 | "dist", 22 | "!dist/typings" 23 | ], 24 | "devDependencies": { 25 | "@compodoc/live-server": "1.2.3", 26 | "@rollup/plugin-node-resolve": "^15.0.2", 27 | "@rollup/plugin-terser": "^0.4.3", 28 | "@rollup/plugin-typescript": "^11.1.1", 29 | "@types/animejs": "3.1.0", 30 | "@types/three": "0.147.1", 31 | "aframe-types": "0.9.1", 32 | "concurrently": "^7.1.0", 33 | "rimraf": "^5.0.1", 34 | "rollup": "^2.71.1", 35 | "rollup-plugin-dts": "^5.3.0", 36 | "typedoc": "^0.24.7", 37 | "typescript": "^5.0.4" 38 | }, 39 | "dependencies": { 40 | "@zip.js/zip.js": "^2.7.20" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /screen-fade/src/index.js: -------------------------------------------------------------------------------- 1 | const VERTEX_SHADER = /*glsl*/` 2 | void main() { 3 | vec3 newPosition = position * 2.0; 4 | gl_Position = vec4(newPosition, 1.0); 5 | }`; 6 | 7 | const FRAGMENT_SHADER = /*glsl*/` 8 | uniform vec3 color; 9 | uniform float intensity; 10 | void main() { 11 | gl_FragColor = vec4(color, intensity); 12 | }`; 13 | 14 | AFRAME.registerComponent('screen-fade', { 15 | schema: { 16 | color: { type: "color", default: "#000000" }, 17 | intensity: { type: "number", default: 0.0, max: 1.0, min: 0.0 } 18 | }, 19 | init: function() { 20 | const geometry = new THREE.PlaneGeometry(1, 1); 21 | this.material = new THREE.ShaderMaterial({ 22 | vertexShader: VERTEX_SHADER, 23 | fragmentShader: FRAGMENT_SHADER, 24 | transparent: true, 25 | depthTest: false, 26 | uniforms: { 27 | color: { value: new THREE.Color(this.data.color) }, 28 | intensity: { value: this.data.intensity } 29 | } 30 | }); 31 | this.fullscreenQuad = new THREE.Mesh(geometry, this.material) 32 | 33 | this.el.setObject3D('fullscreenQuad', this.fullscreenQuad); 34 | }, 35 | update: function() { 36 | this.material.uniforms.color.value.set(this.data.color); 37 | this.material.uniforms.intensity.value = this.data.intensity; 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /screen-fade/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fern-solutions/aframe-screen-fade", 3 | "version": "1.0.2", 4 | "description": "A-Frame component for fading the screen, for both desktop and VR", 5 | "module": "dist/screen-fade.esm.min.js", 6 | "browser": "dist/screen-fade.umd.min.js", 7 | "main": "dist/screen-fade.esm.min.js", 8 | "types": "types/screen-fade.d.ts", 9 | "author": "Noeri Huisman", 10 | "license": "MIT", 11 | "scripts": { 12 | "dev": "concurrently \"rollup -c -w\" \"live-server --port=4000 --no-browser ./example --mount=/js:./dist\"", 13 | "build": "rollup -c", 14 | "watch": "rollup -c -w" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/mrxz/fern-aframe-components" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/mrxz/fern-aframe-components/issues" 22 | }, 23 | "homepage": "https://github.com/mrxz/fern-aframe-components/tree/main/screen-fade#readme", 24 | "keywords": [ 25 | "aframe", 26 | "vr", 27 | "xr", 28 | "webxr" 29 | ], 30 | "files": [ 31 | "dist", 32 | "types" 33 | ], 34 | "devDependencies": { 35 | "@compodoc/live-server": "1.2.3", 36 | "@types/animejs": "3.1.0", 37 | "@types/three": "0.147.1", 38 | "aframe-types": "0.9.1", 39 | "concurrently": "^7.1.0", 40 | "rollup": "^2.71.1", 41 | "rollup-plugin-terser": "^7.0.2", 42 | "typescript": "^5.0.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /effekseer/vendor/README.md: -------------------------------------------------------------------------------- 1 | This typings file comes from EffekseerForWebGL release. Since Effekseer doesn't publish npm packages, the typings file is included in this repository. See the original repository for full source and details: https://github.com/effekseer/EffekseerForWebGL 2 | 3 | ## License 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2011 Effekseer Project 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal in 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 12 | the Software, and to permit persons to whom the Software is furnished to do so, 13 | subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /sky-background/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fern-solutions/aframe-sky-background", 3 | "version": "1.0.4", 4 | "description": "A-Frame primitive for efficiently rendering a sky in the background", 5 | "module": "dist/sky-background.esm.min.js", 6 | "browser": "dist/sky-background.umd.min.js", 7 | "main": "dist/sky-background.esm.min.js", 8 | "types": "types/sky-background.d.ts", 9 | "author": "Noeri Huisman", 10 | "license": "MIT", 11 | "scripts": { 12 | "dev": "concurrently \"rollup -c -w\" \"live-server --port=4000 --no-browser ./example --mount=/js:./dist\"", 13 | "build": "rollup -c", 14 | "watch": "rollup -c -w" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/mrxz/fern-aframe-components" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/mrxz/fern-aframe-components/issues" 22 | }, 23 | "homepage": "https://github.com/mrxz/fern-aframe-components/tree/main/sky-background#readme", 24 | "keywords": [ 25 | "aframe", 26 | "vr", 27 | "xr", 28 | "webxr" 29 | ], 30 | "files": [ 31 | "dist", 32 | "types" 33 | ], 34 | "devDependencies": { 35 | "@compodoc/live-server": "1.2.3", 36 | "@types/animejs": "3.1.0", 37 | "@types/three": "0.147.1", 38 | "aframe-types": "0.9.1", 39 | "concurrently": "^7.1.0", 40 | "rollup": "^2.71.1", 41 | "rollup-plugin-terser": "^7.0.2", 42 | "typescript": "^5.0.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/chunks/github-corner-right.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /motion-controller/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fern-solutions/aframe-motion-controller", 3 | "version": "0.1.0", 4 | "description": "A-Frame component for loading WebXR input profiles and models", 5 | "module": "dist/aframe-motion-controller.esm.min.js", 6 | "browser": "dist/aframe-motion-controller.umd.min.js", 7 | "main": "dist/aframe-motion-controller.esm.min.js", 8 | "types": "dist/aframe-motion-controller.d.ts", 9 | "author": "Noeri Huisman", 10 | "license": "MIT", 11 | "scripts": { 12 | "dev": "concurrently \"rollup -c rollup.config.dev.js -w\" \"live-server --port=4000 --no-browser ./example --mount=/js:./dist --mount=/src:./src\"", 13 | "build": "rollup -c rollup.config.prod.js" 14 | }, 15 | "keywords": [ 16 | "aframe", 17 | "typescript", 18 | "webxr" 19 | ], 20 | "files": [ 21 | "dist", 22 | "!dist/typings" 23 | ], 24 | "devDependencies": { 25 | "@compodoc/live-server": "1.2.3", 26 | "@rollup/plugin-node-resolve": "^15.0.2", 27 | "@rollup/plugin-terser": "^0.4.3", 28 | "@rollup/plugin-typescript": "^11.1.1", 29 | "@types/animejs": "3.1.0", 30 | "@types/three": "0.147.1", 31 | "aframe-types": "0.9.1", 32 | "concurrently": "^7.1.0", 33 | "esbuild": "^0.18.17", 34 | "rimraf": "^5.0.1", 35 | "rollup": "^2.71.1", 36 | "rollup-plugin-dts": "^5.3.0", 37 | "rollup-plugin-esbuild": "^5.0.0", 38 | "typedoc": "^0.24.7", 39 | "typescript": "^5.0.4" 40 | }, 41 | "dependencies": { 42 | "@webxr-input-profiles/motion-controllers": "^1.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/chunks/github-corner-left.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3dof/README.md: -------------------------------------------------------------------------------- 1 | # 3DoF component 2 | [![npm version](https://img.shields.io/npm/v/@fern-solutions/aframe-3dof.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-3dof) 3 | [![npm version](https://img.shields.io/npm/l/@fern-solutions/aframe-3dof.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-3dof) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 6 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 7 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 8 | 9 | This component can be used to render a scene in either monoscopic or stereoscopic 3DoF. Only the orientation of the head is used. The position of the camera can be controlled with the position property. 10 | 11 | Checkout the example: [Online Demo](https://aframe-components.fern.solutions/3dof) | [Source](https://github.com/mrxz/fern-aframe-components/blob/main/3dof/example/index.html) 12 | 13 | ## Usage 14 | Load the script from [npm](https://www.npmjs.com/package/@fern-solutions/aframe-3dof) or add the following script tag: 15 | ```HTML 16 | 17 | ``` 18 | 19 | The `3dof` component can be added to any `a-scene` element as follows: 20 | ```HTML 21 | 22 | ``` -------------------------------------------------------------------------------- /motion-controller/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fern A-Frame Components | Motion Controller 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /fix-fog/README.md: -------------------------------------------------------------------------------- 1 | # Fix Fog 2 | [![npm version](https://img.shields.io/npm/v/@fern-solutions/aframe-fix-fog.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-fix-fog) 3 | [![npm version](https://img.shields.io/npm/l/@fern-solutions/aframe-fix-fog.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-fix-fog) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 6 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 7 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 8 | 9 | This changes the fog depth computation from camera space to world space. This improves the fog effect in VR, most noticeably in case of dense fog. It does this by changing some of the shader chunks of Three.js. For more details, read the dev log: [A-Frame Adventures 02 - Fixing Fog](https://fern.solutions/dev-logs/aframe-adventures-02/) 10 | 11 | Checkout the example: [Online Demo](https://aframe-components.fern.solutions/fix-fog) | [Source](https://github.com/mrxz/fern-aframe-components/blob/main/fix-fog/example/index.html) 12 | 13 | ## Usage 14 | Load the script from [npm](https://www.npmjs.com/package/@fern-solutions/aframe-fix-fog) or add the following script tag: 15 | ```HTML 16 | 17 | ``` 18 | 19 | That's all, it will automatically update the shader chunks :-) -------------------------------------------------------------------------------- /extra-stats/src/index.js: -------------------------------------------------------------------------------- 1 | import { threeAllocStats } from './rStats.three-alloc'; 2 | import { updatedThreeStats } from './rStats.three'; 3 | 4 | AFRAME.registerComponent('extra-stats', { 5 | schema: { 6 | three: { type: "boolean", default: true }, 7 | aframe: { type: "boolean", default: true }, 8 | threeAlloc: { type: "boolean", default: true }, 9 | }, 10 | init: function() { 11 | const scene = this.el; 12 | if(scene.hasAttribute('stats')) { 13 | console.warn("Both 'stats' and 'extra-stats' are added, only one should be used at a time!"); 14 | } 15 | 16 | const plugins = [ 17 | this.data.three ? updatedThreeStats(scene.renderer) : null, 18 | this.data.aframe ? window.aframeStats(scene) : null, 19 | this.data.threeAlloc ? threeAllocStats() : null, 20 | ].filter(x => x !== null); 21 | this.stats = new window.rStats({ 22 | css: [], // Rely on A-Frame stylesheet 23 | values: { 24 | fps: { caption: 'fps', below: 30 }, 25 | }, 26 | groups: [ 27 | { caption: 'Framerate', values: ['fps', 'raf'] } 28 | ], 29 | plugins: plugins 30 | }); 31 | this.statsEl = document.querySelector('.rs-base'); 32 | 33 | scene.addEventListener('enter-vr', () => this.statsEl.classList.add('a-hidden')); 34 | scene.addEventListener('exit-vr', () => this.statsEl.classList.remove('a-hidden')); 35 | }, 36 | tick: function () { 37 | if(!this.stats) { 38 | return; 39 | } 40 | 41 | this.stats('rAF').tick(); 42 | this.stats('FPS').frame(); 43 | this.stats().update(); 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /sky-background/types/sky-background.d.ts: -------------------------------------------------------------------------------- 1 | import "aframe"; 2 | 3 | declare module "aframe" { 4 | export interface Components { 5 | "sky-background": Component<{}>; 6 | } 7 | 8 | export interface Shaders { 9 | "sky-background": Shader<{ 10 | /** The solid color of the sky at the top */ 11 | topColor: { type: 'color', is: 'uniform', default: '#0077ff' }, 12 | /** The solid color of the sky at the bottom */ 13 | bottomColor: { type: 'color', is: 'uniform', default: '#ffffff' }, 14 | /** Offset in meters to 'angle' the gradient a bit */ 15 | offset: { type: 'float', is: 'uniform', default: 120.0 }, 16 | /** Exponent used to blend between the top and bottom color as a function of height */ 17 | exponent: { type: 'float', is: 'uniform', default: 0.9 }, 18 | /** The equirectangular texture to use, disables the gradient sky */ 19 | src: {type: 'map'}, 20 | }>; 21 | } 22 | 23 | export interface Primitives { 24 | /** 25 | * This primitives allows a sky to be added that's either a gradient or an equirectangular skybox. 26 | * In contrast to the built-in `` this doesn't use a sphere geometry. It renders a fullscreen 27 | * triangle covering the far plane, ensuring it's always in the background and more performant. 28 | */ 29 | "a-sky-background": PrimitiveConstructor<'geometry' | 'material' | 'sky-background', { 30 | 'top-color': 'material.topColor', 31 | 'bottom-color': 'material.bottomColor', 32 | 'offset': 'material.offset', 33 | 'exponent': 'material.exponent', 34 | 'src': 'material.src' 35 | }>; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /hud/README.md: -------------------------------------------------------------------------------- 1 | # HUD component 2 | [![npm version](https://img.shields.io/npm/v/@fern-solutions/aframe-hud.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-hud) 3 | [![npm version](https://img.shields.io/npm/l/@fern-solutions/aframe-hud.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-hud) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 6 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 7 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 8 | 9 | This component allows hud elements to be rendered. The elements render in both desktop and VR mode. On desktop the elements appear on the screen (flat), whereas in VR they are projected on a sphere around the user's head. 10 | 11 | The intended usage is mostly for debugging, but can be used for simple overlays as well. 12 | 13 | Checkout the example: [Online Demo](https://aframe-components.fern.solutions/hud) | [Source](https://github.com/mrxz/fern-aframe-components/blob/main/hud/example/index.html) 14 | 15 | ## Usage 16 | Load the script from [npm](https://www.npmjs.com/package/@fern-solutions/aframe-hud) or add the following script tag: 17 | ```HTML 18 | 19 | ``` 20 | 21 | The `a-hud` and `a-hud-element` primitives can be used as children of `` as follows: 22 | ```HTML 23 | 24 | 25 | 26 | 27 | 28 | ``` -------------------------------------------------------------------------------- /motion-controller/src/motion-controller-space.component.ts: -------------------------------------------------------------------------------- 1 | import * as AFRAME from 'aframe'; 2 | 3 | const MotionControllerSpaceComponent = AFRAME.registerComponent('motion-controller-space', { 4 | schema: { 5 | hand: { type: 'string', oneOf: ['left', 'right'], default: 'left' }, 6 | space: { type: 'string', oneOf: ['gripSpace', 'targetRaySpace'], default: 'targetRaySpace' }, 7 | }, 8 | __fields: {} as { 9 | motionControllerSystem: AFRAME.Systems['motion-controller'], 10 | inputSource: XRInputSource|undefined, 11 | }, 12 | init: function() { 13 | this.motionControllerSystem = this.el.sceneEl.systems['motion-controller']; 14 | this.el.sceneEl.addEventListener('motion-controller-change', _event => { 15 | const inputSourceRecord = this.motionControllerSystem[this.data.hand]; 16 | this.inputSource = inputSourceRecord?.xrInputSource; 17 | }); 18 | }, 19 | tick: function() { 20 | const xrFrame = this.el.sceneEl.frame; 21 | const xrReferenceSpace = this.el.sceneEl.renderer.xr.getReferenceSpace?.(); 22 | if(!this.inputSource || !xrFrame || !xrReferenceSpace) { 23 | return; 24 | } 25 | 26 | const xrSpace = this.inputSource[this.data.space]; 27 | if(!xrSpace) { 28 | return; 29 | } 30 | 31 | // FIXME: Consider getting the pose in the system and caching for subsequent retrievals 32 | const xrPose = xrFrame.getPose(xrSpace, xrReferenceSpace); 33 | if(xrPose) { 34 | this.el.object3D.matrix.fromArray(xrPose.transform.matrix); 35 | this.el.object3D.matrix.decompose(this.el.object3D.position, this.el.object3D.quaternion, this.el.object3D.scale); 36 | } 37 | } 38 | }); 39 | 40 | declare module "aframe" { 41 | export interface Components { 42 | "motion-controller-space": InstanceType 43 | } 44 | } -------------------------------------------------------------------------------- /highlight/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fern A-Frame Components | Highlight 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /screen-fade/README.md: -------------------------------------------------------------------------------- 1 | # Screen Fade component 2 | [![npm version](https://img.shields.io/npm/v/@fern-solutions/aframe-screen-fade.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-screen-fade) 3 | [![npm version](https://img.shields.io/npm/l/@fern-solutions/aframe-screen-fade.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-screen-fade) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 6 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 7 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 8 | 9 | This component allows the screen to be faded to and from a solid color. The effect works in both desktop and VR mode. This can be used for situations like loading a scene, handling transitions or snap turning. 10 | 11 | Checkout the example: [Online Demo](https://aframe-components.fern.solutions/screen-fade) | [Source](https://github.com/mrxz/fern-aframe-components/blob/main/screen-fade/example/index.html) 12 | 13 | ## Usage 14 | Load the script from [npm](https://www.npmjs.com/package/@fern-solutions/aframe-screen-fade) or add the following script tag: 15 | ```HTML 16 | 17 | ``` 18 | 19 | The `screen-fade` component can be attached to the `` as follows: 20 | ```HTML 21 | 22 | ``` 23 | 24 | The fade itself can then be controlled using the `intensity` property. 25 | 26 | ## Properties 27 | | Name | Type | Default |Description | 28 | | ---- | ---- | ------- |----------- | 29 | | `color` | color | #000000 | The solid color the screen fades to | 30 | | `intensity` | float | 0.0 | The intensity of the fade between 0.0 and 1.0 | 31 | -------------------------------------------------------------------------------- /extra-stats/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fern A-Frame Components | Extra Stats 5 | 6 | 7 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![fern-aframe-components-banner](https://github.com/mrxz/fern-aframe-components/assets/8823461/a8306bff-071c-4fe0-8606-7820bbbf4bd0)](https://github.com/mrxz/fern-aframe-components/) 2 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 3 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 4 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 5 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 6 | 7 | This is a collection of A-Frame components. Check the individual components for their usages and corresponding examples. 8 | 9 | * [Mirror ``](./mirror): Mirror component and primitive for high-quality mirrors 10 | * [Highlight `highlight`](./highlight): Render a highlight for objects when they are occluded 11 | * [Screen Fade `screen-fade`](./screen-fade): Allows fade transitions in both desktop and VR mode 12 | * [Sky Background ``](./sky-background): Primitive for efficiently rendering a sky in the background 13 | * [Effekseer `effekseer`](./effekseer/): Component for integrating [Effekseer](https://effekseer.github.io/en/) effects into A-Frame projects 14 | * [Extra Stats `extra-stats`](./extra-stats): Replacement for the `stats` component that shows additional stats 15 | * [Fix Fog](./fix-fog): Changes fog depth from camera space to world space, [fixing fog in VR](https://fern.solutions/dev-logs/aframe-adventures-02/) 16 | * [HUD `` and ``](./hud): HUD components for debugging purposes 17 | * [3DoF `3dof`](./3dof): Monoscopic or stereoscopic 3DoF rendering 18 | 19 | ### A-Frame Locomotion 20 | If you're looking for components for locomotion, be sure to check out [aframe-locomotion](https://github.com/mrxz/aframe-locomotion). 21 | 22 | I hope you find these components useful. If you have any suggestions, questions or ideas, feel free to reach out to me. 23 | 24 | Buy Me a Coffee at ko-fi.com 25 | -------------------------------------------------------------------------------- /3dof/src/index.js: -------------------------------------------------------------------------------- 1 | AFRAME.registerComponent('3dof', { 2 | schema: { 3 | position: { type: 'vec3' }, 4 | stereo: { default: true } 5 | }, 6 | init: function() { 7 | const renderer = this.el.sceneEl.renderer; 8 | renderer.xr.cameraAutoUpdate = false; 9 | }, 10 | tick: (function() { 11 | const targetPosition = new THREE.Vector3(); 12 | const eyeCenterPosition = new THREE.Vector3(); 13 | const eyePosition = new THREE.Vector3(); 14 | 15 | return function() { 16 | const renderer = this.el.sceneEl.renderer; 17 | if(!renderer.xr.isPresenting) { 18 | return; 19 | } 20 | targetPosition.copy(this.data.position); 21 | 22 | const xrCamera = renderer.xr.getCamera(); 23 | // Update camera to let THREE compute the center position between the eyes 24 | renderer.xr.updateCamera(xrCamera); 25 | eyeCenterPosition.setFromMatrixPosition(xrCamera.matrixWorld); 26 | 27 | // Place xrCamera at the designated position 28 | xrCamera.matrix.setPosition(targetPosition); 29 | xrCamera.matrixWorld.copy(xrCamera.matrix); 30 | xrCamera.matrixWorldInverse.copy(xrCamera.matrixWorld).invert(); 31 | 32 | // Update camera object (e.g. a-camera) to match new position 33 | // This ensures child object inherit the proper parent transform 34 | const cameraObject = this.el.sceneEl.camera.el.object3D; 35 | cameraObject.matrix.copy(xrCamera.matrix); 36 | cameraObject.matrix.decompose(cameraObject.position, cameraObject.quaternion, cameraObject.scale); 37 | 38 | // Update individual eye cameras 39 | if(this.data.stereo) { 40 | for(const camera of xrCamera.cameras) { 41 | eyePosition.setFromMatrixPosition(camera.matrix).sub(eyeCenterPosition).add(targetPosition); 42 | camera.matrix.setPosition(eyePosition); 43 | camera.matrixWorld.copy(camera.matrix); 44 | camera.matrixWorldInverse.copy(camera.matrixWorld).invert(); 45 | } 46 | } else { 47 | for(const camera of xrCamera.cameras) { 48 | camera.matrix.setPosition(targetPosition); 49 | camera.matrixWorld.copy(camera.matrix); 50 | camera.matrixWorldInverse.copy(camera.matrixWorld).invert(); 51 | } 52 | } 53 | } 54 | })(), 55 | }); -------------------------------------------------------------------------------- /extra-stats/README.md: -------------------------------------------------------------------------------- 1 | # Extra Stats component 2 | [![npm version](https://img.shields.io/npm/v/@fern-solutions/aframe-extra-stats.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-extra-stats) 3 | [![npm version](https://img.shields.io/npm/l/@fern-solutions/aframe-extra-stats.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-extra-stats) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 6 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 7 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 8 | 9 | This component expands on the built-in `stats` component with additional stats. It's intended for debugging and development purposes only. 10 | 11 | Checkout the example: [Online Demo](https://aframe-components.fern.solutions/extra-stats) | [Source](https://github.com/mrxz/fern-aframe-components/blob/main/extra-stats/example/index.html) 12 | 13 | ## Usage 14 | Load the script from [npm](https://www.npmjs.com/package/@fern-solutions/aframe-extra-stats) or add the following script tag: 15 | ```HTML 16 | 17 | ``` 18 | 19 | The `extra-stats` component should be added to an `` and is intended to **_replace_** the `stats` component. Make sure to only add one or the other to the ``. Example: 20 | ```HTML 21 | 22 | 23 | 24 | ``` 25 | 26 | Properties can be used to enable or disable groups. For example, the following results in a stats panel similar to the built-in one: 27 | ```HTML 28 | 29 | 30 | 31 | ``` 32 | 33 | **Note:** The properties can't be changed after initialization, as they determin with which plugins rStats is initialized. 34 | 35 | ## Properties 36 | | Name | Type | Default |Description | 37 | | ---- | ---- | ------- |----------- | 38 | | `three` | boolean | true | Show the Three.js related stats | 39 | | `aframe` | boolean | true | Show the A-Frame related stats ("Load Time" and "Entities") | 40 | | `three-alloc` | boolean | true | Show the Three.js allocations of various types (Vectors, Matrices, Quaternions and Colors) | 41 | -------------------------------------------------------------------------------- /sky-background/README.md: -------------------------------------------------------------------------------- 1 | # Sky Background component 2 | [![npm version](https://img.shields.io/npm/v/@fern-solutions/aframe-sky-background.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-sky-background) 3 | [![npm version](https://img.shields.io/npm/l/@fern-solutions/aframe-sky-background.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-sky-background) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 6 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 7 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 8 | 9 | This primitives allows a sky to be added that's either a gradient or an equirectangular skybox. In contrast to the built-in `` this doesn't use a sphere geometry. It renders a fullscreen triangle covering the far plane, ensuring it's always in the background and more performant. 10 | 11 | Checkout the example: [Online Demo](https://aframe-components.fern.solutions/sky-background) | [Source](https://github.com/mrxz/fern-aframe-components/blob/main/sky-background/example/index.html) 12 | 13 | ## Usage 14 | Load the script from [npm](https://www.npmjs.com/package/@fern-solutions/aframe-sky-background) or add the following script tag: 15 | ```HTML 16 | 17 | ``` 18 | 19 | The `` primitive can be used as follows: 20 | ```HTML 21 | 22 | ``` 23 | Instead of a gradient sky, an equirectangular skybox texture can be used as well: 24 | ```HTML 25 | 26 | ``` 27 | 28 | ## Attributes 29 | | Name | Type | Default |Description | 30 | | ---- | ---- | ------- |----------- | 31 | | `top-color` | color | #0077ff | The solid color of the sky at the top | 32 | | `bottom-color` | color | #ffffff | The solid color of the sky at the bottom | 33 | | `offset` | number | 120 | Offset in meters to 'angle' the gradient a bit | 34 | | `exponent` | number | 0.9 | Exponent used to blend between the top and bottom color as a function of height | 35 | | `src` | map | null | The equirectangular texture to use, disables the gradient sky | 36 | 37 | -------------------------------------------------------------------------------- /mirror/README.md: -------------------------------------------------------------------------------- 1 | # Mirror component 2 | [![npm version](https://img.shields.io/npm/v/@fern-solutions/aframe-mirror.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-mirror) 3 | [![npm version](https://img.shields.io/npm/l/@fern-solutions/aframe-mirror.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-mirror) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 6 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 7 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 8 | 9 | The `mirror` component and corresponding `` primitive allows a high-quality mirror to be added to your A-Frame scenes with ease. Instead of rendering to a texture, it uses a stencil buffer and renders directly into the framebuffer resulting in a high-quality mirror. It works in both desktop and VR mode. 10 | 11 | Inspiration for this came from a snippet from Carmack's Unscripted talk during the Meta Connect 2022 (emphasis mine): 12 | > Almost all the mirrors that you see in VR games are done by rendering a separate view and they're usually **blurrier, aliased, generally not particularly high quality**. While in Home we're doing a neat little trick which is actually what I was doing all the way back in the Doom 3 game, where you sort of flip the world around, render through that cut-out and you can get as high a quality in the mirror as you get looking at things directly. 13 | > Source: https://www.youtube.com/watch?v=ouq5yyzSiAw&t=892s 14 | 15 | Checkout the example: [Online Demo](https://aframe-components.fern.solutions/mirror) | [Source](https://github.com/mrxz/fern-aframe-components/blob/main/mirror/example/index.html) 16 | 17 | ## Usage 18 | Load the script from [npm](https://www.npmjs.com/package/@fern-solutions/aframe-mirror) or add the following script tag: 19 | ```HTML 20 | 21 | ``` 22 | 23 | The `a-mirror` primitive can be added to your scene as follows: 24 | ```HTML 25 | 26 | ``` 27 | 28 | Additionally layers can be specified to show or hide objects in the mirror view. For example, you might want to render the avatar of the user only in the mirror and not the main camera view. 29 | 30 | ## Properties 31 | | Name | Type | Default |Description | 32 | | ---- | ---- | ------- |----------- | 33 | | `layers` | array | [0] | List of layers that should be enabled when rendering the mirror view | 34 | 35 | ## Limitations 36 | * Mirrors are not rendered recursively, so any mirror seen from another mirror will just render as an opaque plane 37 | * Avoid mixing transparency with mirrors (e.g. looking at a mirror through transparent objects). Depending on the render-order this either results in the overlap being an opaque mirror or the transparent object not being visible. 38 | -------------------------------------------------------------------------------- /extra-stats/src/rStats.three-alloc.js: -------------------------------------------------------------------------------- 1 | export function threeAllocStats() { 2 | let _rS = null; 3 | 4 | const _values = { 5 | v2: { 6 | caption: 'Vector2' 7 | }, 8 | v3: { 9 | caption: 'Vector3' 10 | }, 11 | v4: { 12 | caption: 'Vector4' 13 | }, 14 | quat: { 15 | caption: 'Quaternion' 16 | }, 17 | mat3: { 18 | caption: 'Matrix3' 19 | }, 20 | mat4: { 21 | caption: 'Matrix4' 22 | }, 23 | color: { 24 | caption: 'Color' 25 | }, 26 | } 27 | 28 | const keys = ['v2', 'v3', 'v4', 'quat', 'mat3', 'mat4', 'color']; 29 | const _groups = [{ 30 | caption: 'Three.js allocs', 31 | values: keys 32 | }]; 33 | 34 | const counters = {}; 35 | const resetCounters = () => keys.forEach(key => counters[key] = 0); 36 | resetCounters(); 37 | const increment = (key) => counters[key]++; 38 | 39 | function _update() { 40 | keys.forEach(key => _rS(key).set(counters[key])); 41 | resetCounters(); 42 | } 43 | 44 | function _start() { } 45 | 46 | function _end() { } 47 | 48 | class InstrumentedVector2 extends THREE.Vector2 { 49 | constructor() { 50 | super(...arguments); 51 | increment('v2'); 52 | } 53 | } 54 | class InstrumentedVector3 extends THREE.Vector3 { 55 | constructor() { 56 | super(...arguments); 57 | increment('v3'); 58 | } 59 | } 60 | class InstrumentedVector4 extends THREE.Vector4 { 61 | constructor() { 62 | super(...arguments); 63 | increment('v4'); 64 | } 65 | } 66 | class InstrumentedQuaternion extends THREE.Quaternion { 67 | constructor() { 68 | super(...arguments); 69 | increment('quat'); 70 | } 71 | } 72 | class InstrumentedMatrix3 extends THREE.Matrix3 { 73 | constructor() { 74 | super(...arguments); 75 | increment('mat3'); 76 | } 77 | } 78 | class InstrumentedMatrix4 extends THREE.Matrix4 { 79 | constructor() { 80 | super(...arguments); 81 | increment('mat4'); 82 | } 83 | } 84 | class InstrumentedColor extends THREE.Color { 85 | constructor() { 86 | super(...arguments); 87 | increment('color'); 88 | } 89 | } 90 | function _attach(r) { 91 | _rS = r; 92 | THREE.Vector2 = InstrumentedVector2; 93 | THREE.Vector3 = InstrumentedVector3; 94 | THREE.Vector4 = InstrumentedVector4; 95 | THREE.Quaternion = InstrumentedQuaternion; 96 | THREE.Matrix3 = InstrumentedMatrix3; 97 | THREE.Matrix4 = InstrumentedMatrix4; 98 | THREE.Color = InstrumentedColor; 99 | } 100 | 101 | return { 102 | update: _update, 103 | start: _start, 104 | end: _end, 105 | attach: _attach, 106 | values: _values, 107 | groups: _groups, 108 | fractions: [] 109 | }; 110 | } -------------------------------------------------------------------------------- /3dof/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fern A-Frame Components | 3DoF 5 | 6 | 7 | 8 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /mirror/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fern A-Frame Components | Mirror 5 | 6 | 7 | 8 | 9 | 10 | 11 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /highlight/README.md: -------------------------------------------------------------------------------- 1 | # Highlight component 2 | [![npm version](https://img.shields.io/npm/v/@fern-solutions/aframe-highlight.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-highlight) 3 | [![npm version](https://img.shields.io/npm/l/@fern-solutions/aframe-highlight.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-highlight) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 6 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 7 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 8 | 9 | This component adds a highlight to objects. The highlight can be drawn on either the occluded parts (default) or the visible parts (`mode: visible`). This allows the user to easily and quickly spot them. The highlight consists of accentuating the rim of the object. 10 | 11 | Checkout the example: [Online Demo](https://aframe-components.fern.solutions/highlight) | [Source](https://github.com/mrxz/fern-aframe-components/blob/main/highlight/example/index.html) 12 | 13 | ## Usage 14 | Load the script from [npm](https://www.npmjs.com/package/@fern-solutions/aframe-highlight) or add the following script tag: 15 | ```HTML 16 | 17 | ``` 18 | 19 | The `highlight` component can be attached to any object: 20 | ```HTML 21 | 22 | ``` 23 | 24 | To ensure certain objects or entities are rendered on top of any highlight (e.g. hands), you can use the `above-highlight` component: 25 | ```HTML 26 | 27 | ``` 28 | 29 | ## Properties 30 | | Name | Type | Default |Description | 31 | | ---- | ---- | ------- |----------- | 32 | | `rimColor` | color | #FF0000 | The color at the rim of the object | 33 | | `rimOpacity` | float | 1.0 | The opacity of the rim between 0.0 and 1.0 | 34 | | `coreColor` | color | #000000 | The color at the core of the object | 35 | | `coreOpacity` | float | 0.0 | The opacity of the core between 0.0 and 1.0 | 36 | | `mode` | 'occlusion' or 'visible | occlusion | Whether to show the highlight on occluded or visible parts of the object| 37 | 38 | ## Caveats 39 | This component tries to be a relatively lightweight and does not introduce any post-processing. Instead it renders the highlighted objects one (or two) more times to achieve the effect. There are however a couple of caveats associated with this: 40 | 1. If the object is expensive to draw, the highlight rendering can be expensive as well 41 | 2. The component uses an `Object3D` directly attached to the scene with `renderOrder` set to 1000. If you make use of `renderOrder` make sure there is no conflict. 42 | 3. When rendering the highlight on occluded parts, the object itself is rendered without proper depth testing, meaning for concave objects the highlight won't always match the outer surface. 43 | 4. Multiple objects with highlights when occluded can render in arbitrary order. It's recommended to limit the amount of entities this effect is used on and try to make sure these entities don't overlap each other. -------------------------------------------------------------------------------- /effekseer/README.md: -------------------------------------------------------------------------------- 1 | # Effekseer component 2 | [![npm version](https://img.shields.io/npm/v/@fern-solutions/aframe-effekseer.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-effekseer) 3 | [![npm version](https://img.shields.io/npm/l/@fern-solutions/aframe-effekseer.svg?style=flat-square)](https://www.npmjs.com/package/@fern-solutions/aframe-effekseer) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/mrxz/fern-aframe-components/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@noerihuisman/blue?icon=twitter&label)](https://twitter.com/noerihuisman) 6 | [![mastodon](https://flat.badgen.net/badge/mastodon/@noerihuisman@arvr.social/blue?icon=mastodon&label)](https://arvr.social/@noerihuisman) 7 | [![ko-fi](https://img.shields.io/badge/ko--fi-buy%20me%20a%20coffee-ff5f5f?style=flat-square)](https://ko-fi.com/fernsolutions) 8 | 9 | This component allows integrating [Effekseer](https://effekseer.github.io/en/) effects into A-Frame projects. The effects also work in VR, though not in AR ([EffekseerForWebGL#74](https://github.com/effekseer/EffekseerForWebGL/issues/74)). 10 | 11 | Checkout the example: [Online Demo](https://aframe-components.fern.solutions/effekseer) | [Source](https://github.com/mrxz/fern-aframe-components/blob/main/effekseer/example/index.html) 12 | 13 | ## Usage 14 | The setup requires a couple of libraries to be loaded before the component is loaded. For testing the library the below snippet can be copied and used, but for production use it's recommended to bundle your own versions of the relevant dependencies: 15 | ```HTML 16 | 17 | 18 | 19 | 20 | 21 | 22 | ``` 23 | 24 | The `effekseer` system needs to be configured on the `` to load the effekseer wasm: 25 | ```HTML 26 | 27 | 28 | ``` 29 | 30 | Next you can add effects to your scene as follows 31 | ```HTML 32 | 33 | 34 | 35 | (...) 36 | 37 | ``` 38 | 39 | ## Properties 40 | The `effekseer` component supports the following properties: 41 | | Name | Type | Default |Description | 42 | | ---- | ---- | ------- |----------- | 43 | | `src` | asset | | URL or path to the `.efk` or `.efkpkg` (requires zip.js) | 44 | | `autoplay` | boolean | true | Automatically start playing the effect once loaded | 45 | | `loop` | boolean | false | Restart the effect as soon as it ends | 46 | | `dynamic` | boolean | false | Update the world transform of the effect every tick. Allows the effect to move, rotate and scale along with the entity. Only enabled if you need this behaviour, otherwise leave it disabled for performance reasons | 47 | 48 | ## Methods 49 | The component exposes a couple of methods for interacting with the effect: `playEffect()`, `pauseEffect()`, `resumeEffect()`, `stopEffect()` and `setTargetLocation(targetLocation: THREE.Vector3)` 50 | (See example for details on how to use these) -------------------------------------------------------------------------------- /hud/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fern A-Frame Components | HUD 5 | 6 | 7 | 8 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /sky-background/src/index.js: -------------------------------------------------------------------------------- 1 | AFRAME.registerShader('sky-background', { 2 | schema: { 3 | topColor: { type: 'color', is: 'uniform', default: '#0077ff' }, 4 | bottomColor: { type: 'color', is: 'uniform', default: '#ffffff' }, 5 | offset: { type: 'float', is: 'uniform', default: 120.0 }, 6 | exponent: { type: 'float', is: 'uniform', default: 0.9 }, 7 | src: {type: 'map'}, 8 | }, 9 | vertexShader: /* glsl */` 10 | varying vec2 vUv; 11 | 12 | void main() { 13 | vUv = uv*2.0; 14 | gl_Position = vec4(position.xy, 1.0, 1.0); 15 | } 16 | `, 17 | fragmentShader: /* glsl */` 18 | uniform vec3 topColor; 19 | uniform vec3 bottomColor; 20 | uniform float offset; 21 | uniform float exponent; 22 | uniform sampler2D map; 23 | 24 | uniform mat4 cameraWorldMatrix; 25 | uniform mat4 invProjectionMatrix; 26 | 27 | varying vec2 vUv; 28 | 29 | #include 30 | #include 31 | 32 | void main() { 33 | vec2 ndc = 2.0 * vUv - vec2(1.0); 34 | // Convert ndc to ray origin 35 | vec4 rayOrigin4 = cameraWorldMatrix * invProjectionMatrix * vec4( ndc, - 1.0, 1.0 ); 36 | vec3 rayOrigin = rayOrigin4.xyz / rayOrigin4.w; 37 | // Compute ray direction 38 | vec3 rayDirection = normalize( mat3(cameraWorldMatrix) * ( invProjectionMatrix * vec4( ndc, 0.0, 1.0 ) ).xyz ); 39 | 40 | #ifdef USE_MAP 41 | gl_FragColor = vec4(texture(map, equirectUv(rayDirection)).rgb, 1.0); 42 | #else 43 | float h = normalize((rayOrigin + rayDirection * 500.0) + offset).y; 44 | gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0 ), exponent), 0.0)), 1.0); 45 | #endif 46 | 47 | #include 48 | #include 49 | #include 50 | }`, 51 | init: function(data) { 52 | // Handle compatibility with older Three.js versions (A-Frame <1.5.0) 53 | if(+AFRAME.THREE.REVISION < 158) { 54 | this.fragmentShader = this.fragmentShader.replace(/colorspace_fragment/, 'encodings_fragment'); 55 | } 56 | 57 | this.__proto__.__proto__.init.call(this, data); 58 | this.material.uniforms.map = { value: null }; 59 | this.el.addEventListener('materialtextureloaded', e => { 60 | // Mipmaps on equirectangular images causes a seam 61 | e.detail.texture.generateMipmaps = false; 62 | // Unlike built-in materials, having 'material.map' doesn't result in Three.js 63 | // setting the uniform as well, so do it manually. 64 | this.material.uniforms.map.value = e.detail.texture; 65 | }); 66 | }, 67 | update: function(data) { 68 | this.__proto__.__proto__.update.call(this, data); 69 | AFRAME.utils.material.updateMap(this, data); 70 | } 71 | }); 72 | 73 | AFRAME.registerComponent('sky-background', { 74 | init: function() { 75 | const mesh = this.el.getObject3D('mesh'); 76 | mesh.frustumCulled = false; 77 | mesh.material.uniforms.cameraWorldMatrix = { value: new THREE.Matrix4() }; 78 | mesh.material.uniforms.invProjectionMatrix = { value: new THREE.Matrix4() }; 79 | mesh.onBeforeRender = (renderer, scene, camera, geometry, material, group) => { 80 | material.uniforms.cameraWorldMatrix.value.copy(camera.matrixWorld); 81 | material.uniforms.cameraWorldMatrix.needsUpdate = true; 82 | material.uniforms.invProjectionMatrix.value.copy(camera.projectionMatrix).invert(); 83 | material.uniforms.invProjectionMatrix.needsUpdate = true; 84 | } 85 | } 86 | }); 87 | 88 | AFRAME.registerPrimitive('a-sky-background', { 89 | defaultComponents: { 90 | geometry: { 91 | primitive: 'triangle', 92 | vertexA: { x: -1, y: -1, z: 0 }, 93 | vertexB: { x: 3, y: -1, z: 0 }, 94 | vertexC: { x: -1, y: 3, z: 0 }, 95 | }, 96 | material: { 97 | shader: 'sky-background', 98 | 99 | }, 100 | 'sky-background': {}, 101 | }, 102 | mappings: { 103 | 'top-color': 'material.topColor', 104 | 'bottom-color': 'material.bottomColor', 105 | 'offset': 'material.offset', 106 | 'exponent': 'material.exponent', 107 | 'src': 'material.src' 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /effekseer/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fern A-Frame Components | Effekseer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /motion-controller/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | export function phongMaterialFromStandardMaterial(sourceMaterial: THREE.MeshStandardMaterial) { 4 | return new THREE.MeshPhongMaterial({ 5 | color: sourceMaterial.color.clone(), 6 | map: sourceMaterial.map, 7 | side: sourceMaterial.side, 8 | }); 9 | } 10 | 11 | export function occluderMaterialFromStandardMaterial(sourceMaterial: THREE.MeshStandardMaterial) { 12 | return new THREE.MeshBasicMaterial({ 13 | colorWrite: false, 14 | side: sourceMaterial.side, 15 | }); 16 | } 17 | 18 | interface MaterialOnBeforeRender { 19 | onBeforeRender: (renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, geometry: THREE.BufferGeometry, object: THREE.Object3D, group: any) => void; 20 | } 21 | export function hologramMaterialFromStandardMaterial(sourceMaterial: THREE.MeshStandardMaterial) { 22 | const hologramMaterial = new THREE.ShaderMaterial({ 23 | side: sourceMaterial.side, 24 | opacity: 0.4, 25 | transparent: true, 26 | vertexShader: /* glsl */` 27 | #include 28 | #include 29 | #include 30 | 31 | uniform float outline; 32 | 33 | varying vec3 vObjectPosition; 34 | varying vec3 vWorldPosition; 35 | 36 | void main() { 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | 43 | #include 44 | if(outline > 0.0) { 45 | transformed += normal*0.001; 46 | } 47 | #include 48 | #include 49 | 50 | vObjectPosition = position; 51 | 52 | vWorldPosition = mvPosition.xyz; 53 | } 54 | `, 55 | // Note: this shader is hard-coded and optimized for the generic-hand asset 56 | fragmentShader: /* glsl */` 57 | #include 58 | #include 59 | #include 60 | 61 | uniform float outline; 62 | 63 | varying vec3 vObjectPosition; 64 | varying vec3 vWorldPosition; 65 | 66 | void main() { 67 | 68 | if(outline > 0.0) { 69 | float alpha = 0.5 * min((-vObjectPosition.y + 0.09)/0.15, 1.0); 70 | gl_FragColor = vec4(1.0, 1.0, 1.0, alpha); 71 | } else { 72 | vec3 toCamera = normalize(cameraPosition - vWorldPosition); 73 | float factor = pow(1.0 - dot(toCamera, vNormal), 10.0); 74 | 75 | float alpha = 0.5 * min((-vObjectPosition.y + 0.06)/0.15, 1.0); 76 | vec3 color = mix(vec3(0.0), vec3(0.8, 0.8, 1.0), factor); 77 | 78 | gl_FragColor = vec4(color, alpha); 79 | } 80 | 81 | #include 82 | #include 83 | } 84 | `, 85 | uniforms: { 86 | outline: { value: 1.0 } 87 | } 88 | }); 89 | 90 | // Note: onBeforeRender on material lacks types (internal for Three.js), but is slightly more convenient 91 | // in our case, so use it anyway. 92 | (hologramMaterial as unknown as MaterialOnBeforeRender).onBeforeRender = (renderer, scene, camera, geometry, object, group) => { 93 | // Block depth 94 | hologramMaterial.colorWrite = false; 95 | renderer.renderBufferDirect(camera, scene, geometry, hologramMaterial, object, group); 96 | hologramMaterial.colorWrite = true; 97 | // Outline 98 | hologramMaterial.side = THREE.BackSide; 99 | hologramMaterial.uniforms.outline.value = 1.0; 100 | (hologramMaterial.uniforms.outline as any).needsUpdate = true; 101 | hologramMaterial.needsUpdate = true; 102 | renderer.renderBufferDirect(camera, scene, geometry, hologramMaterial, object, group); 103 | // Restore for normal render 104 | hologramMaterial.side = THREE.FrontSide; 105 | hologramMaterial.uniforms.outline.value = 0.0; 106 | (hologramMaterial.uniforms.outline as any).needsUpdate = true; 107 | hologramMaterial.needsUpdate = true; 108 | 109 | } 110 | 111 | return hologramMaterial; 112 | } -------------------------------------------------------------------------------- /effekseer/src/effekseer.component.ts: -------------------------------------------------------------------------------- 1 | import * as AFRAME from 'aframe'; 2 | import * as THREE from 'three'; 3 | import 'effekseer'; 4 | 5 | /** 6 | * Component for rendering an Effekseer effect. 7 | */ 8 | export const EffekseerComponent = AFRAME.registerComponent('effekseer', { 9 | schema: { 10 | /** The .efk or .efkpkg file to use */ 11 | src: { type: 'asset' }, 12 | 13 | /** Whether or not to play the effect as soon as it's loaded */ 14 | autoPlay: { type: 'boolean', default: true }, 15 | /** Whether to loop the effect or not */ 16 | loop: { type: 'boolean', default: false }, 17 | /** Whether or not to update the effects scale, position and rotation each tick */ 18 | dynamic: { type: 'boolean', default: false }, 19 | }, 20 | __fields: {} as { 21 | effect: effekseer.EffekseerEffect|null, 22 | handle: effekseer.EffekseerHandle|null, 23 | 24 | tempMatrixArray: Float32Array, 25 | targetLocation: THREE.Vector3, 26 | }, 27 | init: function() { 28 | this.tempMatrixArray = new Float32Array(16); 29 | this.targetLocation = new THREE.Vector3(); 30 | }, 31 | update: function(oldData) { 32 | if(oldData.src !== this.data.src) { 33 | // Ask system for the effect 34 | this.effect = null; 35 | if(this.handle) { 36 | this.handle.stop(); 37 | this.handle = null; 38 | } 39 | if(this.data.src) { 40 | const loadingSrc = this.data.src; 41 | this.system.getOrLoadEffect(this.data.src).then(effect => { 42 | // Make sure the loaded effect is still the intended effect 43 | if(this.data.src !== loadingSrc) { 44 | return; 45 | } 46 | this.effect = effect; 47 | // Request a handle 48 | if(this.data.autoPlay) { 49 | // Ensure the objects matrix world is set 50 | this.el.object3D.updateMatrixWorld(); 51 | this.playEffect(); 52 | } 53 | }).catch(reason => { 54 | console.error(`Failed to load effect ${this.data.src}: ${reason}`); 55 | if(this.data.src === loadingSrc) { 56 | this.effect = null; 57 | this.handle = null; 58 | } 59 | }); 60 | } 61 | } 62 | }, 63 | updateTransform: function() { 64 | this.el.object3D.matrixWorld.toArray(this.tempMatrixArray); 65 | this.handle!.setMatrix(this.tempMatrixArray); 66 | }, 67 | /** 68 | * Starts a new playback of the effect. This doesn't stop any already playing 69 | * effects associated with this component. Call {@link stopEffect()} for that instead 70 | */ 71 | playEffect: function() { 72 | this.handle = this.system.playEffect(this.effect!); 73 | this.updateTransform(); 74 | this.setTargetLocation(this.targetLocation); 75 | }, 76 | /** 77 | * Pauses the playback of the effect 78 | */ 79 | pauseEffect: function() { 80 | this.handle?.setPaused(true); 81 | }, 82 | /** 83 | * Resumes the effect in case it has been paused 84 | */ 85 | resumeEffect: function() { 86 | this.handle?.setPaused(false); 87 | }, 88 | /** 89 | * Stops the effect 90 | */ 91 | stopEffect: function() { 92 | this.handle?.stop(); 93 | this.handle = null; 94 | }, 95 | /** 96 | * Sets the target location for effects that make use of this. 97 | * This is NOT the location of the effect, but the location the effect _targets_ on. 98 | * Not all effects make use of this location. 99 | * @param target The location the effect should target on 100 | */ 101 | setTargetLocation: function(target: THREE.Vector3) { 102 | this.targetLocation.copy(target); 103 | this.handle?.setTargetLocation(target.x, target.y, target.z); 104 | }, 105 | tick: function() { 106 | if(!this.handle) { 107 | return; 108 | } 109 | 110 | // FIXME: It seems not all effects have a natural end 111 | // preventing them from looping at all. 112 | if(this.data.loop && !this.handle.exists) { 113 | this.playEffect(); 114 | } 115 | 116 | if(this.data.dynamic) { 117 | this.updateTransform(); 118 | } 119 | }, 120 | remove: function() { 121 | this.handle?.stop(); 122 | } 123 | }); 124 | 125 | declare module "aframe" { 126 | interface Components { 127 | "effekseer": InstanceType 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /effekseer/src/effekseer.system.ts: -------------------------------------------------------------------------------- 1 | import * as AFRAME from 'aframe'; 2 | import * as THREE from 'three'; 3 | import 'effekseer'; 4 | import * as zip from '@zip.js/zip.js'; 5 | 6 | const createUnzip = async function(buffer: Uint8Array) { 7 | const cachedFiles: {[key: string]: Uint8Array} = {}; 8 | const reader = new zip.ZipReader(new zip.Uint8ArrayReader(buffer)); 9 | const entries = await reader.getEntries({}); 10 | await Promise.allSettled(entries.map(async e => { 11 | const writer = new zip.Uint8ArrayWriter(); 12 | cachedFiles[e.filename] = await e.getData!(writer); 13 | })); 14 | 15 | return function(_buffer: Uint8Array) { 16 | return { 17 | decompress(file: string) { 18 | return cachedFiles[file]; 19 | } 20 | } 21 | }; 22 | } 23 | 24 | /** 25 | * System for managing the Effekseer context and handling rendering of the effects 26 | */ 27 | export const EffekseerSystem = AFRAME.registerSystem('effekseer', { 28 | schema: { 29 | /** URL to the effekseer.wasm file */ 30 | wasmPath: { type: "string" }, 31 | /** Frame-rate at which the effects are played back */ 32 | frameRate: { type: "number", default: 60.0 } 33 | }, 34 | 35 | __fields: {} as { 36 | getContext: Promise, 37 | context: effekseer.EffekseerContext, 38 | effects: Map, 39 | 40 | fileLoader: THREE.FileLoader, 41 | sentinel: THREE.Mesh, 42 | }, 43 | 44 | init: function() { 45 | this.effects = new Map(); 46 | this.fileLoader = new THREE.FileLoader().setResponseType('arraybuffer'); 47 | 48 | const renderer = this.el.sceneEl.renderer; 49 | this.getContext = new Promise((resolve, reject) => { 50 | effekseer.initRuntime(this.data.wasmPath, () => { 51 | this.context = effekseer.createContext(); 52 | this.context.init(renderer.getContext(), { 53 | instanceMaxCount: 2000, 54 | squareMaxCount: 8000, 55 | }); 56 | this.context.setRestorationOfStatesFlag(false); 57 | resolve(this.context); 58 | }, () => { 59 | reject('Failed to load effekseer wasm') 60 | }); 61 | }) 62 | 63 | // Create a sentinel 64 | const sentinel = new THREE.Mesh(); 65 | sentinel.frustumCulled = false; 66 | (sentinel.material as THREE.MeshBasicMaterial).transparent = true; 67 | sentinel.renderOrder = Number.MAX_VALUE; 68 | this.sentinel = sentinel; 69 | this.el.sceneEl.object3D.add(this.sentinel); 70 | 71 | sentinel.onAfterRender = (renderer, _scene, camera) => { 72 | if(!this.context) { 73 | return; 74 | } 75 | const renderTarget = renderer.getRenderTarget(); 76 | 77 | this.context.setProjectionMatrix(Float32Array.from(camera.projectionMatrix.elements)); 78 | this.context.setCameraMatrix(Float32Array.from(camera.matrixWorldInverse.elements)); 79 | this.context.draw(); 80 | 81 | renderer.resetState(); 82 | renderer.setRenderTarget(renderTarget); 83 | } 84 | }, 85 | 86 | getOrLoadEffect(src: string): Promise { 87 | if(this.effects.has(src)) { 88 | return Promise.resolve(this.effects.get(src)!); 89 | } 90 | 91 | return this.fileLoader.loadAsync(src).then(buffer => new Promise((resolve, reject) => { 92 | this.getContext.then(_ => { 93 | const basePath = src.substring(0, src.lastIndexOf('/') + 1); 94 | let effect: effekseer.EffekseerEffect; 95 | const onload = () => { 96 | // The onload callback doesn't provide the effect as an argument, 97 | // so use a timeout to ensure the return of loadEffect has taken place. 98 | setTimeout(() => resolve(effect!), 0) 99 | }; 100 | if(src.endsWith(".efkpkg")) { 101 | // Note: While the library 'handles' unzipping it does so synchronously 102 | // Since zip.js is async, handle the unzipping upfront. 103 | createUnzip(new Uint8Array(buffer as ArrayBuffer)).then(Unzip => { 104 | effect = this.context.loadEffectPackage(buffer, Unzip, 1.0, 105 | onload, 106 | reject); 107 | this.effects.set(src, effect); 108 | }) 109 | } else { 110 | effect = this.context.loadEffect(buffer, 1.0, 111 | onload, 112 | reject, 113 | (path) => { 114 | // Paths are relative to src 115 | return basePath + path; 116 | }); 117 | this.effects.set(src, effect); 118 | } 119 | }); 120 | })); 121 | }, 122 | 123 | playEffect(effect: effekseer.EffekseerEffect): effekseer.EffekseerHandle { 124 | // Note: entire transform matrix is set briefly after, so simply pass origin here 125 | return this.context.play(effect, 0, 0, 0); 126 | }, 127 | 128 | tick: function(_t, dt) { 129 | if(!this.context) { 130 | return; 131 | } 132 | this.context.update(dt/1000.0 * this.data.frameRate); 133 | } 134 | }); 135 | 136 | declare module "aframe" { 137 | interface Systems { 138 | "effekseer": InstanceType, 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /fix-fog/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fern A-Frame Components | Fix Fog 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /hud/src/index.js: -------------------------------------------------------------------------------- 1 | AFRAME.registerPrimitive('a-hud', { 2 | defaultComponents: { 3 | hud: { }, 4 | }, 5 | mappings: { 6 | radius: 'hud.radius', 7 | 'horizontal-fov': 'hud.horizontalFov', 8 | 'vertical-fov': 'hud.verticalFov', 9 | 'scale-factor': 'hud.scaleFactor', 10 | } 11 | }); 12 | 13 | AFRAME.registerPrimitive('a-hud-element', { 14 | defaultComponents: { 15 | 'hud-element': { }, 16 | }, 17 | mappings: { 18 | align: 'hud-element.align', 19 | anchor: 'hud-element.anchor', 20 | 'content-size': 'hud-element.contentSize', 21 | 'hud-size': 'hud-element.hudSize', 22 | } 23 | }); 24 | 25 | AFRAME.registerComponent('hud', { 26 | schema: { 27 | radius: { type: 'number', default: 1 }, 28 | horizontalFov: { type: 'number', default: 80 }, 29 | verticalFov: { type: 'number', default: 60 }, 30 | scaleFactor: { type: 'number', default: 1.0 }, 31 | }, 32 | init: function() { 33 | this.flat = true; 34 | this.el.sceneEl.addEventListener('rendererresize', () => { 35 | // Relayout is only needed on resize if the layout is flat (= screen space) 36 | if(this.flat) { 37 | this.relayout() 38 | } 39 | }); 40 | this.el.sceneEl.addEventListener('enter-vr', () => { 41 | this.flat = false; 42 | this.relayout(); 43 | }); 44 | this.el.sceneEl.addEventListener('exit-vr', () => { 45 | this.flat = true; 46 | this.relayout(); 47 | }); 48 | }, 49 | relayout: function() { 50 | for(const child of this.el.children) { 51 | if(child.components['hud-element']) { 52 | child.components['hud-element'].layout(this) 53 | } 54 | }; 55 | }, 56 | convertCoordinates: function(coordinates, outV3) { 57 | if(this.flat) { 58 | const camera = this.el.sceneEl.camera; 59 | const yScale = Math.tan(THREE.MathUtils.DEG2RAD * 0.5 * camera.fov) / camera.zoom; 60 | const xScale = yScale * camera.aspect; 61 | 62 | outV3.set(coordinates.x * xScale, coordinates.y * yScale, -1); 63 | } else { 64 | // Compute spherical coordinates 65 | const theta = - this.data.horizontalFov/2.0 * coordinates.x; 66 | const phi = -90 + this.data.verticalFov/2.0 * coordinates.y; 67 | 68 | outV3.set( 69 | Math.sin(THREE.MathUtils.DEG2RAD*phi) * Math.sin(THREE.MathUtils.DEG2RAD*theta), 70 | Math.cos(THREE.MathUtils.DEG2RAD*phi), 71 | Math.sin(THREE.MathUtils.DEG2RAD*phi) * Math.cos(THREE.MathUtils.DEG2RAD*theta)); 72 | outV3.multiplyScalar(this.data.radius); 73 | } 74 | }, 75 | convertWidth: function(width) { 76 | return this.flat ? width * this.data.scaleFactor : width; 77 | }, 78 | convertHeight: function(height) { 79 | // Height is given in "width percentage", so needs to be adjusted based on aspect ratio. 80 | const adjustedHeight = height * this.aspectRatio(); 81 | return this.flat ? adjustedHeight * this.data.scaleFactor : adjustedHeight; 82 | }, 83 | aspectRatio: function() { 84 | if(this.flat) { 85 | return this.el.sceneEl.camera.aspect; 86 | } 87 | return this.data.horizontalFov / this.data.verticalFov; 88 | }, 89 | scale: function() { 90 | if(this.flat) { 91 | const camera = this.el.sceneEl.camera; 92 | const yScale = Math.tan(THREE.MathUtils.DEG2RAD * 0.5 * camera.fov) / camera.zoom; 93 | return 2.0 * yScale * camera.aspect * this.data.scaleFactor; 94 | } 95 | return this.data.horizontalFov/360 * this.data.radius*Math.PI*2.0; 96 | }, 97 | orientate: (function() { 98 | const tempMat4 = new THREE.Matrix4(); 99 | const up = new THREE.Vector3(0, 1, 0); 100 | const origin = new THREE.Vector3(0, 0, 0); 101 | return function(position, outQuaternion) { 102 | if(this.flat) { 103 | outQuaternion.identity(); 104 | return; 105 | } 106 | 107 | tempMat4.lookAt(origin, position, up); 108 | outQuaternion.setFromRotationMatrix(tempMat4); 109 | }; 110 | })() 111 | }); 112 | 113 | /* Normalized coordinates lookup for anchor/align points */ 114 | const COORDINATES = { 115 | 'top-left': new THREE.Vector2(-1.0, 1.0), 116 | 'top': new THREE.Vector2(0.0, 1.0), 117 | 'top-right': new THREE.Vector2(1.0, 1.0), 118 | 'right': new THREE.Vector2(1.0, 0.0), 119 | 'bottom-right': new THREE.Vector2(1.0, -1.0), 120 | 'bottom': new THREE.Vector2(0.0, -1.0), 121 | 'bottom-left': new THREE.Vector2(-1.0, -1.0), 122 | 'left': new THREE.Vector2(-1.0, 0.0), 123 | 'center': new THREE.Vector2(0.0, 0.0), 124 | }; 125 | 126 | AFRAME.registerComponent('hud-element', { 127 | schema: { 128 | align: { type: 'string', default: 'center' }, 129 | anchor: { type: 'string', default: 'center' }, 130 | contentSize: { type: 'vec2', default: new THREE.Vector2(1.0, 1.0) }, 131 | hudSize: { type: 'number', default: 1.0 }, 132 | }, 133 | init: function() { 134 | this.coordinates = new THREE.Vector2(); 135 | }, 136 | update: function() { 137 | const hud = this.el.parentElement.components['hud']; 138 | if(hud) { 139 | this.layout(hud); 140 | } 141 | }, 142 | layout: function(hud) { 143 | const aspect = this.data.contentSize.y / this.data.contentSize.x; 144 | 145 | const coordinates = this.coordinates.copy(COORDINATES[this.data.align]); 146 | const anchor = COORDINATES[this.data.anchor]; 147 | coordinates.x -= anchor.x * hud.convertWidth(this.data.hudSize); 148 | coordinates.y -= anchor.y * hud.convertHeight(this.data.hudSize * aspect); 149 | hud.convertCoordinates(coordinates, this.el.object3D.position); 150 | 151 | const scale = hud.scale() * this.data.hudSize / this.data.contentSize.x; 152 | this.el.object3D.scale.set(scale, scale, scale); 153 | 154 | hud.orientate(this.el.object3D.position, this.el.object3D.quaternion); 155 | } 156 | }); -------------------------------------------------------------------------------- /highlight/src/index.js: -------------------------------------------------------------------------------- 1 | const VERTEX_SHADER = /*glsl*/` 2 | #include 3 | #include 4 | #include 5 | 6 | out vec3 vNormal; 7 | 8 | void main() { 9 | vNormal = normalMatrix * normal; 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | }`; 16 | 17 | let FRAGMENT_SHADER = /*glsl*/` 18 | #include 19 | #include 20 | #include 21 | 22 | uniform vec3 coreColor; 23 | uniform float coreOpacity; 24 | uniform vec3 rimColor; 25 | uniform float rimOpacity; 26 | 27 | in vec3 vNormal; 28 | 29 | void main() { 30 | float factor = 1.0 - max(0.0, dot(normalize(vNormal), vec3(0.0, 0.0, 1.0))); 31 | vec3 color = mix(coreColor, rimColor, factor); 32 | gl_FragColor = vec4(color, (factor * rimOpacity) + ((1.0 - factor) * coreOpacity)); 33 | 34 | #include 35 | #include 36 | #include 37 | #include 38 | }`; 39 | 40 | // Handle compatibility with older Three.js versions (A-Frame 1.4.2) 41 | if(+AFRAME.THREE.REVISION < 158) { 42 | FRAGMENT_SHADER = FRAGMENT_SHADER.replace(/colorspace_fragment/, 'encodings_fragment'); 43 | } 44 | 45 | AFRAME.registerSystem('highlight', { 46 | callbacks: [], 47 | afterCallbacks: [], 48 | init: function() { 49 | // Create a sentinel entity 50 | this.sentinel = new THREE.Mesh(); 51 | this.sentinel.frustumCulled = false; 52 | this.sentinel.material.transparent = true; 53 | // FIXME: What if the application already uses render order for a different purpose? 54 | this.sentinel.renderOrder = 1000; 55 | this.el.object3D.add(this.sentinel) 56 | 57 | this.sentinel.onAfterRender = (renderer, scene, camera) => { 58 | this.callbacks.forEach(cb => cb(renderer, scene, camera)); 59 | this.afterCallbacks.forEach(cb => cb(renderer, scene, camera)) 60 | } 61 | }, 62 | registerCallback: function(callback) { 63 | this.callbacks.push(callback); 64 | }, 65 | unregisterCallback: function(callback) { 66 | const index = this.callbacks.indexOf(callback); 67 | if(index !== -1) { 68 | this.callbacks.splice(index, 1); 69 | } 70 | }, 71 | registerAfterCallback: function(callback) { 72 | this.afterCallbacks.push(callback); 73 | }, 74 | unregisterAfterCallback: function(callback) { 75 | const index = this.afterCallbacks.indexOf(callback); 76 | if(index !== -1) { 77 | this.afterCallbacks.splice(index, 1); 78 | } 79 | } 80 | }); 81 | 82 | const HIGHLIGHT_ON_BEFORE_RENDER_HOOK = "_HIGHLIGHT_ON_BEFORE_RENDER_HOOK_"; 83 | AFRAME.registerComponent('highlight', { 84 | schema: { 85 | coreColor: { type: "color", default: "#000000" }, 86 | coreOpacity: { type: "number", default: 0.0, min: 0.0, max: 1.0 }, 87 | rimColor: { type: "color", default: "#FF0000" }, 88 | rimOpacity: { type: "number", default: 1.0, min: 0.0, max: 1.0 }, 89 | mode: { type: "string", default: "occlusion" }, // occlusion, visible 90 | }, 91 | modes: { 92 | "occlusion": { depthFunc: THREE.GreaterDepth, secondRender: true }, 93 | "visible": { depthFunc: THREE.EqualDepth, secondRender: false }, 94 | }, 95 | init: function() { 96 | this.renderCalls = []; 97 | 98 | this.outlineMaterial = new THREE.ShaderMaterial({ 99 | vertexShader: VERTEX_SHADER, 100 | fragmentShader: FRAGMENT_SHADER, 101 | transparent: true, 102 | depthTest: true, 103 | depthFunc: this.modes[this.data.mode].depthFunc, 104 | depthWrite: false, 105 | uniforms: { 106 | coreColor: { value: new THREE.Color(this.data.coreColor) }, 107 | coreOpacity: { value: this.data.coreOpacity }, 108 | rimColor: { value: new THREE.Color(this.data.rimColor) }, 109 | rimOpacity: { value: this.data.rimOpacity }, 110 | } 111 | }); 112 | 113 | const setupRenderHooks = (object3D) => { 114 | object3D.traverse(c => { 115 | if(c.isMesh) { 116 | if(c.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender && !c.onBeforeRender[HIGHLIGHT_ON_BEFORE_RENDER_HOOK]) { 117 | console.warn("Object already has an onBeforeRender hook! Highlight effect will likely not work for this entity!"); 118 | return; 119 | } 120 | 121 | const captureRenderCallHook = (renderer, scene, camera, geometry, material, group) => { 122 | this.renderCalls.push({ 123 | renderer, scene, camera, geometry, material, group, object: c 124 | }); 125 | }; 126 | captureRenderCallHook[HIGHLIGHT_ON_BEFORE_RENDER_HOOK] = true; 127 | 128 | c.onBeforeRender = captureRenderCallHook; 129 | } 130 | }); 131 | } 132 | // Add hooks to existing meshes. 133 | setupRenderHooks(this.el.object3D); 134 | this.el.addEventListener('object3dset', e => { 135 | setupRenderHooks(e.detail.object); 136 | }); 137 | 138 | this.callback = (renderer, scene, camera) => { 139 | this.renderCalls.forEach(renderCall => { 140 | const { renderer, scene, camera, geometry, material, group, object } = renderCall; 141 | renderer.renderBufferDirect(camera, scene, geometry, this.outlineMaterial, object, group); 142 | 143 | if(this.modes[this.data.mode].secondRender) { 144 | renderer.renderBufferDirect(camera, scene, geometry, material, object, group); 145 | } 146 | }); 147 | this.renderCalls.splice(0, this.renderCalls.length); 148 | } 149 | this.system.registerCallback(this.callback); 150 | }, 151 | update: function() { 152 | this.outlineMaterial.uniforms.coreColor.value.set(this.data.coreColor); 153 | this.outlineMaterial.uniforms.coreOpacity.value = this.data.coreOpacity; 154 | this.outlineMaterial.uniforms.rimColor.value.set(this.data.rimColor); 155 | this.outlineMaterial.uniforms.rimOpacity.value = this.data.rimOpacity; 156 | }, 157 | remove: function() { 158 | this.system.unregisterCallback(this.callback); 159 | } 160 | }); 161 | 162 | AFRAME.registerComponent('above-highlight', { 163 | init: function() { 164 | this.renderCalls = []; 165 | 166 | const setupRenderHooks = (object3D) => { 167 | object3D.traverse(c => { 168 | if(c.isMesh) { 169 | if(c.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender && !c.onBeforeRender[HIGHLIGHT_ON_BEFORE_RENDER_HOOK]) { 170 | console.warn("Object already has an onBeforeRender hook! Highlight effect will likely not work for this entity!"); 171 | return; 172 | } 173 | 174 | const captureRenderCallHook = (renderer, scene, camera, geometry, material, group) => { 175 | this.renderCalls.push({ 176 | renderer, scene, camera, geometry, material, group, object: c 177 | }); 178 | }; 179 | captureRenderCallHook[HIGHLIGHT_ON_BEFORE_RENDER_HOOK] = true; 180 | 181 | c.onBeforeRender = captureRenderCallHook; 182 | } 183 | }); 184 | } 185 | // Add hooks to existing meshes. 186 | setupRenderHooks(this.el.object3D); 187 | this.el.addEventListener('object3dset', e => { 188 | setupRenderHooks(e.detail.object); 189 | }); 190 | 191 | this.callback = (renderer, scene, camera) => { 192 | this.renderCalls.forEach(renderCall => { 193 | const { renderer, scene, camera, geometry, material, group, object } = renderCall; 194 | renderer.renderBufferDirect(camera, scene, geometry, material, object, group); 195 | }); 196 | this.renderCalls.splice(0, this.renderCalls.length); 197 | } 198 | this.el.sceneEl.systems.highlight.registerAfterCallback(this.callback); 199 | }, 200 | remove: function() { 201 | this.el.sceneEl.systems.highlight.unregisterAfterCallback(this.callback); 202 | } 203 | }); -------------------------------------------------------------------------------- /motion-controller/src/motion-controller.system.ts: -------------------------------------------------------------------------------- 1 | import * as AFRAME from 'aframe'; 2 | import { SceneEvent } from 'aframe'; 3 | import { Component, fetchProfile, MotionController } from '@webxr-input-profiles/motion-controllers'; 4 | 5 | const DEFAULT_INPUT_PROFILE_ASSETS_URI = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets/dist/profiles'; 6 | const HANDS_PROFILE_ID = 'generic-hand'; 7 | 8 | export interface InputSourceRecord { 9 | xrInputSource: XRInputSource, 10 | motionController?: MotionController, 11 | componentState: {[key: string]: Component['values'] }, 12 | jointState?: {poses: Float32Array, radii: Float32Array}, 13 | }; 14 | 15 | const MotionControllerSystem = AFRAME.registerSystem('motion-controller', { 16 | schema: { 17 | /** Base URI for fetching profiles and controller models */ 18 | profilesUri: { type: 'string', default: DEFAULT_INPUT_PROFILE_ASSETS_URI }, 19 | /** Enable or disable hand tracking (= pose for hand controllers) */ 20 | enableHandTracking: { type: 'boolean', default: true }, 21 | /** Whether or not input sources representing hands should be reported or not */ 22 | enableHands: { type: 'boolean', default: true }, 23 | }, 24 | __fields: {} as { 25 | /* Currently active XR session */ 26 | xrSession: XRSession|null; 27 | /* List of active input sources */ 28 | inputSources: Array 29 | 30 | /* Dedicated slots for left/right hand for convenience */ 31 | left: InputSourceRecord|null, 32 | right: InputSourceRecord|null, 33 | }, 34 | init: function() { 35 | this.inputSources = []; 36 | this.left = null; 37 | this.right = null; 38 | 39 | if(this.data.enableHands && this.data.enableHandTracking) { 40 | this.sceneEl.setAttribute('webxr', {optionalFeatures: ['hand-tracking']}); 41 | } 42 | 43 | const onInputSourcesChange = (event: XRInputSourceChangeEvent) => { 44 | event.removed.forEach(xrInputSource => { 45 | const index = this.inputSources.findIndex(inputSourceRecord => inputSourceRecord.xrInputSource === xrInputSource); 46 | if(index !== -1) { 47 | // TODO: Notify any component that holds it exclusively 48 | const [removed] = this.inputSources.splice(index, 1); 49 | if(this.left === removed) { 50 | this.left = null; 51 | } 52 | if(this.right === removed) { 53 | this.right = null; 54 | } 55 | } 56 | }); 57 | event.added.forEach(async xrInputSource => { 58 | // Ensure the xrInputSource is relevant (only allow hands if hands are enabled) 59 | if(!this.data.enableHands && xrInputSource.profiles.includes(HANDS_PROFILE_ID)) { 60 | return; 61 | } 62 | 63 | const record: InputSourceRecord = { xrInputSource, componentState: {} }; 64 | this.inputSources.push(record); 65 | // FIXME: Detect and report when there are multiple input sources with the same handedness 66 | if(xrInputSource.handedness === 'left') { 67 | this.left = record; 68 | } else if(xrInputSource.handedness === 'right') { 69 | this.right = record; 70 | } 71 | const { profile, assetPath } = await fetchProfile(xrInputSource, this.data.profilesUri); 72 | record.motionController = new MotionController(xrInputSource, profile, assetPath!); 73 | for(const componentKey in record.motionController!.components) { 74 | const component = record.motionController.components[componentKey]; 75 | record.componentState[componentKey] = {...component.values}; 76 | } 77 | 78 | // Special treatment for hand-tracking 79 | if(record.motionController!.id === HANDS_PROFILE_ID) { 80 | record.jointState = { 81 | poses: new Float32Array(16 * 25), 82 | radii: new Float32Array(25), 83 | }; 84 | } 85 | 86 | // Notify anyone interested in this change 87 | this.sceneEl.emit('motion-controller-change' as keyof AFRAME.EntityEvents); 88 | }); 89 | 90 | // Notify anyone interested in this change 91 | this.sceneEl.emit('motion-controller-change' as keyof AFRAME.EntityEvents); 92 | } 93 | 94 | this.el.sceneEl.addEventListener('enter-vr', _ => { 95 | this.xrSession = this.el.sceneEl.xrSession!; 96 | if(this.xrSession) { 97 | this.xrSession.addEventListener('inputsourceschange', onInputSourcesChange); 98 | } 99 | }); 100 | this.el.sceneEl.addEventListener('exit-vr', _ => { 101 | if(this.xrSession) { 102 | this.xrSession.removeEventListener('inputsourceschange', onInputSourcesChange); 103 | this.xrSession = null; 104 | // Remove any input sources, as the session has ended 105 | this.inputSources.splice(0, this.inputSources.length); 106 | this.left = null; 107 | this.right = null; 108 | this.sceneEl.emit('motion-controller-change' as keyof AFRAME.EntityEvents); 109 | } 110 | }); 111 | }, 112 | tick: function() { 113 | // Update all motion controllers. This ensures that any code 114 | // polling the state gets up to date information, even when not visualized 115 | // FIXME: System tick happens after component ticks, meaning update is always 1 frame late :-/ 116 | this.inputSources.forEach(inputSourceRecord => { 117 | if(!inputSourceRecord.motionController) { 118 | return; 119 | } 120 | 121 | // Let the motion controller library update the state 122 | inputSourceRecord.motionController.updateFromGamepad() 123 | if(inputSourceRecord.motionController.id === HANDS_PROFILE_ID) { 124 | this.updateHandJoints(inputSourceRecord); 125 | } 126 | const hand = this.left === inputSourceRecord ? 'left' : this.right === inputSourceRecord ? 'right' : undefined; 127 | 128 | // Compare the state with the last recorded state, and emit events for any changes 129 | for(const componentKey in inputSourceRecord.motionController.components) { 130 | const newState = inputSourceRecord.motionController?.components[componentKey]!; 131 | const oldState = inputSourceRecord.componentState[componentKey]; 132 | const eventDetails: ButtonEventDetails = { 133 | inputSource: inputSourceRecord.xrInputSource, 134 | motionController: inputSourceRecord.motionController, 135 | hand, 136 | button: componentKey, 137 | buttonState: newState.values, 138 | }; 139 | // Update state already so event handlers will see the new state when polling 140 | const oldButtonState = oldState.state; 141 | oldState.state = newState.values.state; 142 | const oldXAxis = oldState.xAxis; 143 | oldState.xAxis = newState.values.xAxis; 144 | const oldYAxis = oldState.yAxis; 145 | oldState.yAxis = newState.values.yAxis; 146 | 147 | if(newState.values.state !== oldButtonState) { 148 | if(oldButtonState === 'touched') { 149 | // No longer touched -> touchend 150 | this.el.emit('touchend', eventDetails); 151 | } else if(oldButtonState === 'pressed') { 152 | // No longer pressed -> buttonup 153 | this.el.emit('buttonup', eventDetails); 154 | } 155 | 156 | if(newState.values.state === 'touched') { 157 | // Now touched -> touchstart 158 | this.el.emit('touchstart', eventDetails); 159 | } else if(newState.values.state === 'pressed') { 160 | // Now pressed -> buttondown 161 | this.el.emit('buttondown', eventDetails); 162 | } 163 | } 164 | 165 | if(newState.type === 'thumbstick' || newState.type === 'touchpad') { 166 | if(oldXAxis !== newState.values.xAxis || oldYAxis !== newState.values.yAxis) { 167 | // Value along axis changed 168 | this.el.emit('axismove', eventDetails); 169 | } 170 | } 171 | } 172 | }); 173 | }, 174 | updateHandJoints: function(inputSourceRecord: InputSourceRecord) { 175 | const xrFrame = this.el.sceneEl.frame; 176 | if(!xrFrame) { 177 | return; 178 | } 179 | 180 | // Make sure the hand is present 181 | const hand = inputSourceRecord.xrInputSource.hand; 182 | if(!hand) { 183 | return; 184 | } 185 | 186 | // Note: @types/webxr misses quite a few of the hand tracking types 187 | (xrFrame as any).fillPoses(hand.values(), inputSourceRecord.xrInputSource.gripSpace, inputSourceRecord.jointState!.poses); 188 | // FIXME: Perhaps only fetch radii once or upon request(?) 189 | (xrFrame as any).fillJointRadii(hand.values(), inputSourceRecord.jointState!.radii); 190 | } 191 | }); 192 | 193 | export interface ButtonEventDetails { 194 | inputSource: XRInputSource; 195 | motionController: MotionController 196 | hand?: 'left'|'right'; 197 | button: string; 198 | buttonState: Component['values'] 199 | } 200 | 201 | declare module "aframe" { 202 | export interface Systems { 203 | "motion-controller": InstanceType 204 | } 205 | export interface SceneEvents { 206 | "motion-controller-change": SceneEvent<{}> 207 | "touchstart": SceneEvent 208 | "touchend": SceneEvent 209 | "buttondown": SceneEvent 210 | "buttonup": SceneEvent 211 | "buttonchange": SceneEvent 212 | "axismove": SceneEvent 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /motion-controller/src/motion-controller-model.component.ts: -------------------------------------------------------------------------------- 1 | import * as AFRAME from 'aframe'; 2 | import * as THREE from 'three'; 3 | import { MotionController, VisualResponse } from '@webxr-input-profiles/motion-controllers'; 4 | import { hologramMaterialFromStandardMaterial, occluderMaterialFromStandardMaterial, phongMaterialFromStandardMaterial } from './utils'; 5 | import { HAND_JOINT_NAMES } from './hand-joint-names'; 6 | import { InputSourceRecord } from './motion-controller.system'; 7 | 8 | /* The below code is based on Three.js: https://github.com/mrdoob/three.js/blob/dev/examples/jsm/webxr/XRControllerModelFactory.js 9 | * MIT LICENSE 10 | * Copyright © 2010-2023 three.js authors 11 | */ 12 | 13 | type EnhancedVisualResponse = VisualResponse & { 14 | valueNode?: THREE.Object3D, 15 | minNode?: THREE.Object3D, 16 | maxNode?: THREE.Object3D 17 | }; 18 | 19 | const MotionControllerModelComponent = AFRAME.registerComponent('motion-controller-model', { 20 | schema: { 21 | hand: { type: 'string', oneOf: ['left', 'right'], default: 'left' }, 22 | overrideMaterial: { type: 'string', oneOf: ['none', 'phong', 'occluder'], default: 'phong'}, 23 | overrideHandMaterial: { type: 'string', oneOf: ['none', 'phong', 'occluder', 'hologram'], default: 'hologram'}, 24 | buttonTouchColor: { type: 'color', default: '#8AB' }, 25 | buttonPressColor: { type: 'color', default: '#2DF' } 26 | }, 27 | __fields: {} as { 28 | motionControllerSystem: AFRAME.Systems['motion-controller'], 29 | inputSourceRecord: InputSourceRecord|null, 30 | motionController: MotionController|null, 31 | componentMeshes: Map>, 32 | // Only relevant for hand tracking models 33 | handJoints: Array 34 | }, 35 | init: function() { 36 | this.motionControllerSystem = this.el.sceneEl.systems['motion-controller']; 37 | this.componentMeshes = new Map(); 38 | this.handJoints = new Array(25); 39 | const gltfLoader = new AFRAME.THREE.GLTFLoader(); 40 | this.el.sceneEl.addEventListener('motion-controller-change', _event => { 41 | const inputSourceRecord = this.motionControllerSystem[this.data.hand]; 42 | this.inputSourceRecord = inputSourceRecord; 43 | if(inputSourceRecord && inputSourceRecord.motionController) { 44 | gltfLoader.load(inputSourceRecord.motionController.assetUrl, (gltf) => { 45 | // Make sure the motionController is still the one the model was loaded for 46 | if(this.motionController !== inputSourceRecord.motionController) { 47 | return; 48 | } 49 | this.el.setObject3D('mesh', gltf.scene); 50 | const isHandModel = this.motionController?.id === 'generic-hand'; 51 | 52 | // Traverse the mesh to change materials and extract references to hand joints 53 | gltf.scene.traverse(child => { 54 | if(!(child as any).isMesh) { 55 | return; 56 | } 57 | 58 | // Extract bones from skinned mesh (as these are likely hand joints) 59 | if(isHandModel && child.type === 'SkinnedMesh') { 60 | const skinnedMesh = child as THREE.SkinnedMesh; 61 | const bones = skinnedMesh.skeleton.bones; 62 | for(const bone of bones) { 63 | const index = HAND_JOINT_NAMES.indexOf(bone.name); 64 | if(index !== -1) { 65 | this.handJoints[index] = bone; 66 | } 67 | } 68 | 69 | // Exclude them from frustum culling 70 | skinnedMesh.frustumCulled = false; 71 | } 72 | 73 | // The default materials might be physical based ones requiring an environment map 74 | // for proper rendering. Since this isn't always desirable, convert to phong material instead. 75 | const mesh = child as THREE.Mesh; 76 | switch(isHandModel ? this.data.overrideHandMaterial : this.data.overrideMaterial) { 77 | case 'phong': 78 | mesh.material = phongMaterialFromStandardMaterial(mesh.material as THREE.MeshStandardMaterial); 79 | break; 80 | case 'occluder': 81 | mesh.material = occluderMaterialFromStandardMaterial(mesh.material as THREE.MeshStandardMaterial); 82 | break; 83 | case 'hologram': 84 | mesh.material = hologramMaterialFromStandardMaterial(mesh.material as THREE.MeshStandardMaterial); 85 | break; 86 | } 87 | }); 88 | 89 | this.componentMeshes.clear(); 90 | Object.values(this.motionController!.components).forEach((component) => { 91 | // Can't traverse the rootNodes of the components, as these are hardly ever correct. 92 | // See: https://github.com/immersive-web/webxr-input-profiles/issues/249 93 | const componentMeshes: Array<{mesh: THREE.Mesh, originalColor: THREE.Color}> = []; 94 | this.componentMeshes.set(component.id, componentMeshes); 95 | 96 | // Enhance the visual responses with references to the actual Three.js objects from the loaded glTF 97 | Object.values(component.visualResponses).forEach((visualResponse) => { 98 | const valueNode = gltf.scene.getObjectByName(visualResponse.valueNodeName); 99 | const minNode = visualResponse.minNodeName ? gltf.scene.getObjectByName(visualResponse.minNodeName) : undefined; 100 | const maxNode = visualResponse.maxNodeName ? gltf.scene.getObjectByName(visualResponse.maxNodeName) : undefined; 101 | 102 | if(!valueNode) { 103 | console.error('Missing value node in model for visualResponse: ', visualResponse.componentProperty); 104 | return; 105 | } 106 | 107 | // Extract meshes from valueNodes 108 | valueNode.traverse(c => { 109 | if(c.type === 'Mesh') { 110 | const mesh = c as THREE.Mesh; 111 | const originalColor = (mesh.material as THREE.MeshPhongMaterial).color.clone(); 112 | componentMeshes.push({mesh, originalColor}); 113 | } 114 | }); 115 | 116 | // Enhance VisualResponse with references to the actual nodes 117 | (visualResponse as EnhancedVisualResponse).valueNode = valueNode; 118 | if(visualResponse.valueNodeProperty === 'transform') { 119 | if(!minNode || !maxNode) { 120 | console.error('Missing value node in model for visualResponse: ', visualResponse.componentProperty); 121 | (visualResponse as EnhancedVisualResponse).valueNode = undefined; 122 | return; 123 | } 124 | (visualResponse as EnhancedVisualResponse).minNode = minNode; 125 | (visualResponse as EnhancedVisualResponse).maxNode = maxNode; 126 | } 127 | }); 128 | }); 129 | }); 130 | this.motionController = inputSourceRecord.motionController; 131 | } else { 132 | // TODO: Remove mesh 133 | if(this.el.getObject3D('mesh')) { 134 | this.el.removeObject3D('mesh'); 135 | } 136 | for(let i = 0; i < 25; i++) { 137 | this.handJoints[i] = undefined; 138 | } 139 | this.motionController = null; 140 | } 141 | }); 142 | }, 143 | remove: function() { 144 | // TODO: Clean-up any event handlers 145 | // TODO: Remove controller mesh from scene 146 | // TODO: Remove enhanced properties from motion controller instances(?) 147 | }, 148 | tick: function() { 149 | if(!this.motionController || !this.el.getObject3D('mesh')) { // FIXME: Improve check for mesh 150 | return; 151 | } 152 | 153 | // Hand joints 154 | if(this.inputSourceRecord?.jointState) { 155 | for(let i = 0; i < 25; i++) { 156 | const joint = this.handJoints[i]!; 157 | joint.matrix.fromArray(this.inputSourceRecord!.jointState!.poses, i * 16); 158 | joint.matrix.decompose(joint.position, joint.quaternion, joint.scale); 159 | } 160 | } 161 | 162 | // Components and visual responses 163 | for(const componentId in this.motionController.components) { 164 | const component = this.motionController.components[componentId]; 165 | 166 | // Update node data based on the visual responses' current states 167 | Object.values(component.visualResponses).forEach((visualResponse) => { 168 | const { valueNode, minNode, maxNode, value, valueNodeProperty } = visualResponse as EnhancedVisualResponse; 169 | 170 | // Skip if the visual response node is not found. No error is needed, 171 | // because it will have been reported at load time. 172 | if(!valueNode) return; 173 | 174 | // Calculate the new properties based on the weight supplied 175 | if(valueNodeProperty === 'visibility') { 176 | valueNode.visible = value as boolean; 177 | } else if(valueNodeProperty === 'transform') { 178 | valueNode.quaternion.slerpQuaternions( 179 | minNode!.quaternion, 180 | maxNode!.quaternion, 181 | value as number 182 | ); 183 | valueNode.position.lerpVectors( 184 | minNode!.position, 185 | maxNode!.position, 186 | value as number 187 | ); 188 | } 189 | }); 190 | 191 | // Update color based on state 192 | // FIXME: Parse colors once instead of using the string representations 193 | let color: string|null = null; 194 | if(component.values.state === 'touched') { 195 | color = this.data.buttonTouchColor; 196 | } else if(component.values.state === 'pressed') { 197 | color = this.data.buttonPressColor; 198 | } 199 | this.componentMeshes.get(componentId)?.forEach(mesh => { 200 | // FIXME: This depends on the color of the controller model whether this is visible or not 201 | // Find a better way to colorize it, while maintaining texture 202 | (mesh.mesh.material as THREE.MeshPhongMaterial).color.set(color || mesh.originalColor); 203 | }); 204 | } 205 | } 206 | }); 207 | 208 | declare module "aframe" { 209 | export interface Components { 210 | "motion-controller-model": InstanceType 211 | } 212 | } -------------------------------------------------------------------------------- /mirror/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System for keeping track of any portal (incl. mirrors). 3 | * Keeps track of the (active) portals and render them at the 4 | * end of normal rendering. 5 | */ 6 | AFRAME.registerSystem('portal', { 7 | schema: {}, 8 | portals: [], 9 | init: function() { 10 | // Prevent auto clearing for each render 11 | const renderer = this.sceneEl.renderer; 12 | renderer.autoClear = false; 13 | renderer.info.autoReset = false; 14 | 15 | // No-op onAfterRender 16 | const nopAfterRender = function() {}; 17 | 18 | // Create a sentinel 19 | const sentinel = new THREE.Mesh(); 20 | sentinel.frustumCulled = false; 21 | sentinel.material.transparent = true; 22 | sentinel.renderOrder = Number.MAX_VALUE; 23 | this.sentinel = sentinel; 24 | this.sceneEl.object3D.add(this.sentinel); 25 | 26 | sentinel.onAfterRender = (renderer, scene, camera) => { 27 | // In case of XR, only call the render hooks for the last camera (e.g. right eye) 28 | if(renderer.xr.isPresenting) { 29 | const cameras = renderer.xr.getCamera().cameras; 30 | if(camera != cameras[cameras.length - 1]) { 31 | return; 32 | } 33 | } 34 | sentinel.visible = false; 35 | this.portals.forEach(portal => portal.setInactive()); 36 | 37 | // Supress A-Frame's scene.onAfterRender callback during portal/mirror rendering 38 | const oldOnAfterRender = scene.onAfterRender; 39 | scene.onAfterRender = nopAfterRender; 40 | 41 | // Let portals and mirrors render themselves 42 | this.portals.forEach(portal => portal.render(renderer, scene, camera)); 43 | 44 | this.portals.forEach(portal => portal.setActive()); 45 | sentinel.visible = true; 46 | scene.onAfterRender = oldOnAfterRender; 47 | } 48 | }, 49 | tick: function() { 50 | // Note: by default A-frame doesn't sort objects for rendering 51 | // so manually ensure the sentinel is at the tail end 52 | const sceneObject = this.sceneEl.object3D; 53 | if(sceneObject.children[sceneObject.children.length - 1] !== this.sentinel) { 54 | sceneObject.add(this.sentinel); 55 | } 56 | 57 | this.sceneEl.renderer.info.reset(); 58 | }, 59 | registerPortal: function(portal) { 60 | this.portals.push(portal); 61 | portal.setPortalId(this.portals.length); 62 | }, 63 | unregisterPortal: function(portal) { 64 | const index = this.portals.indexOf(portal); 65 | if(index !== -1) { 66 | this.portals.splice(index, 1); 67 | this.portals.forEach((portal, index) => portal.setPortalId(index + 1)); 68 | } 69 | } 70 | }); 71 | 72 | /** 73 | * Base logic for portals 74 | */ 75 | const baseComponent = { 76 | schema: { 77 | layers: { type: 'array', default: [0] } 78 | }, 79 | init: function() { 80 | const mesh = this.el.getObject3D('mesh'); 81 | 82 | // Setup the material of the portal (write to stencil, adhere to depth) 83 | this.surfaceMaterial = mesh.material; 84 | const material = this.surfaceMaterial; 85 | material.transparent = true; 86 | material.colorWrite = false; 87 | material.depthWrite = true; 88 | material.stencilWrite = true; 89 | material.depthFunc = THREE.LessEqualDepth; 90 | material.stencilFunc = THREE.AlwaysStencilFunc; 91 | material.stencilZPass = THREE.ReplaceStencilOp; 92 | material.stencilZFail = THREE.KeepStencilOp; 93 | 94 | // Register mirror (which gives it its id) 95 | this.el.sceneEl.systems['portal'].registerPortal(this); 96 | material.stencilRef = this.portalId; 97 | 98 | // Use onBeforeRender to determine if the mirror is inside the frustum 99 | this.insideFrustum = false; 100 | mesh.onBeforeRender = () => { 101 | this.insideFrustum = true; 102 | }; 103 | 104 | // Layers for visibility 105 | this.layers = new THREE.Layers(); 106 | this.layers.disableAll(); 107 | 108 | // Temporary camera objects to hold the state before reflecting 109 | this.tempCamera = new THREE.PerspectiveCamera(); 110 | this.tempCameras = [new THREE.PerspectiveCamera(), new THREE.PerspectiveCamera()]; 111 | 112 | // Setup clipping plane 113 | this.clippingPlane = new THREE.Plane(); 114 | 115 | // Utility for copying camera properties 116 | this.copyCamera = function(source, target) { 117 | target.matrixWorld.copy(source.matrixWorld); 118 | target.matrixWorldInverse.copy(source.matrixWorldInverse); 119 | target.projectionMatrix.copy(source.projectionMatrix); 120 | target.layers.mask = source.layers.mask; 121 | } 122 | 123 | // Monkey patch setMaterial on WebGLState 124 | const oldWebGLStateSetMaterialFn = this.el.sceneEl.renderer.state.setMaterial; 125 | const webGLStateSetMaterialFn = function(material, frontFaceCW) { 126 | oldWebGLStateSetMaterialFn(material, !frontFaceCW); 127 | }; 128 | this.unpatchWebGLState = function(state) { 129 | state.setMaterial = oldWebGLStateSetMaterialFn; 130 | } 131 | this.patchWebGLState = function(state) { 132 | state.setMaterial = webGLStateSetMaterialFn; 133 | } 134 | 135 | // Temp variables 136 | this._mirrorPos = new THREE.Vector3(); 137 | this._mirrorQuat = new THREE.Quaternion(); 138 | this._cameraPos = new THREE.Vector3(); 139 | this._cameraLPos = new THREE.Vector3(); 140 | this._cameraRPos = new THREE.Vector3(); 141 | this._normal = new THREE.Vector3(); 142 | this._adjustMatrix = new THREE.Matrix4(); 143 | }, 144 | setPortalId: function(id) { 145 | this.portalId = id; 146 | this.surfaceMaterial.stencilRef = id; 147 | }, 148 | setInactive: function() { 149 | this.surfaceMaterial.stencilWrite = false; 150 | }, 151 | setActive: function() { 152 | this.surfaceMaterial.stencilWrite = true; 153 | }, 154 | update: function() { 155 | this.layers.disableAll(); 156 | this.data.layers.map(x => this.layers.enable(+x)); 157 | }, 158 | preRender: function() {}, 159 | postRender: function() {}, 160 | render: function(renderer, scene, camera) { 161 | // Only render if the portal surface is inside the frustum 162 | if(!this.insideFrustum) { 163 | return; 164 | } 165 | this.insideFrustum = false; 166 | 167 | // Temporarily move the camera 168 | const sceneCamera = renderer.xr.isPresenting ? renderer.xr.getCamera() : this.tempCamera; 169 | 170 | // Make sure the portal surface can be seen 171 | let visible; 172 | const mirrorPos = this.el.object3D.getWorldPosition(this._mirrorPos); 173 | const n = this._normal.set(0, 0, 1); 174 | n.applyQuaternion(this.el.object3D.getWorldQuaternion(this._mirrorQuat)); 175 | if(renderer.xr.isPresenting) { 176 | const cameras = sceneCamera.cameras; 177 | this._cameraLPos.setFromMatrixPosition(cameras[0].matrixWorld); 178 | this._cameraRPos.setFromMatrixPosition(cameras[1].matrixWorld); 179 | visible = 180 | this._cameraLPos.subVectors(mirrorPos, this._cameraLPos).dot(n) <= 0.0 || 181 | this._cameraRPos.subVectors(mirrorPos, this._cameraRPos).dot(n) <= 0.0; 182 | 183 | } else { 184 | const view = camera.getWorldPosition(this._cameraPos).subVectors(mirrorPos, this._cameraPos); 185 | visible = view.dot(n) <= 0.0; 186 | } 187 | if(!visible) { 188 | return; 189 | } 190 | 191 | // The portal surface is visible, so compute the clipping plane 192 | this.createClippingPlane(this.clippingPlane); 193 | 194 | // Callback to allow adjustments before rendering the portal contents 195 | if(this.onBeforeRender) { 196 | this.onBeforeRender(renderer, scene, camera, this); 197 | } 198 | 199 | // Construct a matrix for rendering the other side of the portal 200 | const adjustMatrix = this.createAdjustMatrix(this._adjustMatrix); 201 | 202 | // Update camera(s) for rendering the portal contents 203 | if(renderer.xr.isPresenting) { 204 | // Use temp-cameras to store camera matrices 205 | const cameras = sceneCamera.cameras; 206 | this.copyCamera(sceneCamera, this.tempCamera); 207 | for(let i = 0; i < cameras.length; i++) { 208 | this.copyCamera(cameras[i], this.tempCameras[i]); 209 | 210 | cameras[i].matrixWorld.premultiply(adjustMatrix); 211 | cameras[i].matrixWorldInverse.copy(cameras[i].matrixWorld).invert(); 212 | cameras[i].layers.mask = this.layers.mask; 213 | } 214 | 215 | // Set projection matrix for frustum culling 216 | setProjectionFromUnion(sceneCamera, cameras[0], cameras[1]); 217 | 218 | // Apply clipping plane in projection matrix 219 | adjustProjectionMatrix(cameras[0], this.clippingPlane); 220 | adjustProjectionMatrix(cameras[1], this.clippingPlane); 221 | } else { 222 | sceneCamera.near = camera.near; 223 | sceneCamera.far = camera.far; 224 | sceneCamera.projectionMatrix.copy(camera.projectionMatrix); 225 | 226 | sceneCamera.matrix.copy(camera.matrixWorld).premultiply(adjustMatrix); 227 | sceneCamera.matrix.decompose( 228 | sceneCamera.position, 229 | sceneCamera.quaternion, 230 | sceneCamera.scale); 231 | sceneCamera.matrixWorld.copy(sceneCamera.matrix); 232 | sceneCamera.matrixWorldInverse.copy(sceneCamera.matrix).invert(); 233 | adjustProjectionMatrix(sceneCamera, this.clippingPlane); 234 | } 235 | 236 | // Hide portal surface 237 | const mesh = this.el.getObject3D('mesh'); 238 | mesh.visible = false; 239 | 240 | // Render portal contents 241 | renderer.xr.cameraAutoUpdate = false; 242 | this.preRender(renderer); 243 | renderer.state.buffers.stencil.setTest(true); 244 | renderer.state.buffers.stencil.setFunc(THREE.EqualStencilFunc, this.portalId, 0xFF); 245 | renderer.state.buffers.stencil.setOp(THREE.KeepStencilOp, THREE.KeepStencilOp, THREE.KeepStencilOp); 246 | renderer.state.buffers.stencil.setLocked(true); 247 | 248 | renderer.clearDepth(); 249 | const oldLayersMask = sceneCamera.layers.mask; 250 | sceneCamera.layers.mask = this.layers.mask; 251 | const oldMatrixWorldAutoUpdate = scene.matrixWorldAutoUpdate; 252 | scene.matrixWorldAutoUpdate = false; 253 | renderer.render(scene, this.tempCamera); 254 | scene.matrixWorldAutoUpdate = oldMatrixWorldAutoUpdate; 255 | sceneCamera.layers.mask = oldLayersMask; 256 | 257 | renderer.state.buffers.stencil.setLocked(false); 258 | this.postRender(renderer); 259 | renderer.xr.cameraAutoUpdate = true; 260 | 261 | // Restore portal surface 262 | mesh.visible = true; 263 | 264 | // Restore cameras (in case of XR) 265 | if(renderer.xr.isPresenting) { 266 | const cameras = sceneCamera.cameras; 267 | this.copyCamera(this.tempCamera, sceneCamera); 268 | for(let i = 0; i < cameras.length; i++) { 269 | this.copyCamera(this.tempCameras[i], cameras[i]); 270 | } 271 | } 272 | 273 | // Callback to allow adjustments after rendering the portal contents 274 | if(this.onAfterRender) { 275 | this.onAfterRender(renderer, scene, camera, this); 276 | } 277 | }, 278 | remove: function() { 279 | this.el.sceneEl.systems['portal'].unregisterPortal(this); 280 | } 281 | }; 282 | 283 | AFRAME.registerComponent('mirror', { 284 | ...baseComponent, 285 | createClippingPlane: function(plane) { 286 | const mirrorPos = this.el.object3D.getWorldPosition(this._mirrorPos); 287 | const n = this._normal.set(0, 0, 1); 288 | n.applyQuaternion(this.el.object3D.getWorldQuaternion(this._mirrorQuat)); 289 | const d = -mirrorPos.dot(n); 290 | return plane.set(n, d); 291 | }, 292 | createAdjustMatrix: function(matrix) { 293 | const n = this.clippingPlane.normal; 294 | const d = this.clippingPlane.constant; 295 | return matrix.set( 296 | 1 -2*n.x*n.x, -2*n.x*n.y, -2*n.x*n.z, -2*n.x*d, 297 | -2*n.x*n.y, 1-2*n.y*n.y, -2*n.y*n.z, -2*n.y*d, 298 | -2*n.x*n.z, -2*n.y*n.z, 1-2*n.z*n.z, -2*n.z*d, 299 | 0, 0, 0, 1 300 | ); 301 | }, 302 | preRender: function(renderer) { 303 | this.patchWebGLState(renderer.state); 304 | }, 305 | postRender: function(renderer) { 306 | this.unpatchWebGLState(renderer.state); 307 | } 308 | }); 309 | 310 | AFRAME.registerComponent('portal', { 311 | ...baseComponent, 312 | schema: { 313 | ...baseComponent.schema, 314 | destination: { type: 'selector' } 315 | }, 316 | createClippingPlane: function(plane) { 317 | // Clipping plane depends on the destination 318 | const destinationPos = this.data.destination.object3D.getWorldPosition(this._mirrorPos); 319 | const n = this._normal.set(0, 0, 1); 320 | n.applyQuaternion(this.data.destination.object3D.getWorldQuaternion(this._mirrorQuat)); 321 | const d = -destinationPos.dot(n); 322 | return plane.set(n, d); 323 | }, 324 | rotate180Matrix: new THREE.Matrix4().makeRotationY(Math.PI), 325 | createAdjustMatrix: function(matrix) { 326 | matrix.copy(this.el.object3D.matrixWorld); 327 | return matrix.invert() 328 | .premultiply(this.rotate180Matrix) 329 | .premultiply(this.data.destination.object3D.matrixWorld); 330 | } 331 | }); 332 | 333 | /* Primitives */ 334 | AFRAME.registerPrimitive('a-mirror', { 335 | defaultComponents: { 336 | geometry: { primitive: 'plane' }, 337 | mirror: {} 338 | }, 339 | mappings: { 340 | layers: 'mirror.layers', 341 | } 342 | }); 343 | 344 | AFRAME.registerPrimitive('a-portal', { 345 | defaultComponents: { 346 | geometry: { primitive: 'plane' }, 347 | portal: {} 348 | }, 349 | mappings: { 350 | layers: 'portal.layers', 351 | destination: 'portal.destination', 352 | } 353 | }); 354 | 355 | /* Utils */ 356 | const adjustProjectionMatrix = (function() { 357 | const _tempV4 = new THREE.Vector4(); 358 | const _tempPlane = new THREE.Plane(); 359 | const _q = new THREE.Vector4(); 360 | return function(sceneCamera, clippingPlane) { 361 | _tempPlane.copy(clippingPlane).applyMatrix4(sceneCamera.matrixWorldInverse); 362 | const clipPlane = _tempV4.set(_tempPlane.normal.x, _tempPlane.normal.y, _tempPlane.normal.z, _tempPlane.constant); 363 | const projectionMatrix = sceneCamera.projectionMatrix; 364 | 365 | _q.x = (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0]; 366 | _q.y = (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5]; 367 | _q.z = -1.0; 368 | _q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14]; 369 | 370 | // Calculate the scaled plane vector 371 | clipPlane.multiplyScalar(2.0 / clipPlane.dot(_q)); 372 | 373 | projectionMatrix.elements[2] = clipPlane.x; 374 | projectionMatrix.elements[6] = clipPlane.y; 375 | projectionMatrix.elements[10] = clipPlane.z + 1.0 + 0.0; 376 | projectionMatrix.elements[14] = clipPlane.w; 377 | }; 378 | })(); 379 | 380 | const setProjectionFromUnion = (function() { 381 | const _cameraLPos = new THREE.Vector3(); 382 | const _cameraRPos = new THREE.Vector3(); 383 | 384 | // Note: this method is straight from THREE.js WebXRManager.js 385 | // See: https://github.com/mrdoob/three.js/blob/8fd3b2acbd08952deee1e40c18b00907c5cd4c4d/src/renderers/webxr/WebXRManager.js#L429 386 | // Its replicated here since we do need its behaviour, but can't use the rest 387 | // of the XR camera auto updating logic. 388 | // Falls under The MIT License: 389 | // Copyright © 2010-2023 three.js authors 390 | return function(camera, cameraL, cameraR) { 391 | _cameraLPos.setFromMatrixPosition(cameraL.matrixWorld); 392 | _cameraRPos.setFromMatrixPosition(cameraR.matrixWorld); 393 | 394 | const ipd = _cameraLPos.distanceTo(_cameraRPos); 395 | 396 | const projL = cameraL.projectionMatrix.elements; 397 | const projR = cameraR.projectionMatrix.elements; 398 | 399 | // VR systems will have identical far and near planes, and 400 | // most likely identical top and bottom frustum extents. 401 | // Use the left camera for these values. 402 | const near = projL[ 14 ] / ( projL[ 10 ] - 1 ); 403 | const far = projL[ 14 ] / ( projL[ 10 ] + 1 ); 404 | const topFov = ( projL[ 9 ] + 1 ) / projL[ 5 ]; 405 | const bottomFov = ( projL[ 9 ] - 1 ) / projL[ 5 ]; 406 | 407 | const leftFov = ( projL[ 8 ] - 1 ) / projL[ 0 ]; 408 | const rightFov = ( projR[ 8 ] + 1 ) / projR[ 0 ]; 409 | const left = near * leftFov; 410 | const right = near * rightFov; 411 | 412 | // Calculate the new camera's position offset from the 413 | // left camera. xOffset should be roughly half `ipd`. 414 | const zOffset = ipd / ( - leftFov + rightFov ); 415 | const xOffset = zOffset * - leftFov; 416 | 417 | // TODO: Better way to apply this offset? 418 | cameraL.matrixWorld.decompose( camera.position, camera.quaternion, camera.scale ); 419 | camera.translateX( xOffset ); 420 | camera.translateZ( zOffset ); 421 | camera.matrixWorld.compose( camera.position, camera.quaternion, camera.scale ); 422 | camera.matrixWorldInverse.copy( camera.matrixWorld ).invert(); 423 | 424 | // Find the union of the frustum values of the cameras and scale 425 | // the values so that the near plane's position does not change in world space, 426 | // although must now be relative to the new union camera. 427 | const near2 = near + zOffset; 428 | const far2 = far + zOffset; 429 | const left2 = left - xOffset; 430 | const right2 = right + ( ipd - xOffset ); 431 | const top2 = topFov * far / far2 * near2; 432 | const bottom2 = bottomFov * far / far2 * near2; 433 | 434 | camera.projectionMatrix.makePerspective( left2, right2, top2, bottom2, near2, far2 ); 435 | } 436 | })(); 437 | 438 | // Stencil buffer isn't enabled by default since Three.js r163 439 | if(parseInt(AFRAME.THREE.REVISION) >= 163) { 440 | document.addEventListener('render-target-loaded', e => { 441 | let rendererAttrString = e.target.getAttribute('renderer') ?? ''; 442 | if(!/stencil\s*:\s*true/g.test(rendererAttrString)) { 443 | console.warn('[aframe-mirror] Mirror component requires a stencil buffer, enabling it. Add `renderer="stencil: true"` to your to get rid of this warning.') 444 | rendererAttrString += ';stencil:true'; 445 | } 446 | e.target.setAttribute('renderer', rendererAttrString); 447 | }) 448 | } -------------------------------------------------------------------------------- /effekseer/vendor/effekseer.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare namespace effekseer { 3 | 4 | /** 5 | * Initialize Effekseer.js. 6 | * This function must be called at first if use WebAssembly 7 | * @param {string} path A file of webassembply 8 | * @param {function=} onload A function that is called at loading complete 9 | * @param {function=} onerror A function that is called at loading error. 10 | */ 11 | export function initRuntime(path: string, onload: () => void, onerror: () => void): void; 12 | 13 | /** 14 | * Create a context to render in multiple scenes 15 | * @returns {EffekseerContext} context 16 | */ 17 | export function createContext(): EffekseerContext; 18 | 19 | /** 20 | * Release specified context. After that, don't touch a context 21 | * @param {EffekseerContext} context context 22 | */ 23 | export function releaseContext(context: EffekseerContext): void; 24 | 25 | /** 26 | * Set the flag whether Effekseer show logs 27 | * @param {boolean} flag 28 | */ 29 | export function setSetLogEnabled(flag: boolean): void; 30 | 31 | /** 32 | * Set the string of cross origin for images 33 | * @param {string} crossOrigin 34 | */ 35 | export function setImageCrossOrigin(crossOrigin: string): void; 36 | 37 | /** 38 | * Initialize graphics system. 39 | * @param {WebGLRenderingContext} webglContext WebGL Context 40 | * @param {object} settings Some settings with Effekseer initialization 41 | */ 42 | export function init(webglContext: WebGLRenderingContext, settings?: object): void; 43 | 44 | /** 45 | * Advance frames. 46 | * @param {number=} deltaFrames number of advance frames 47 | */ 48 | export function update(deltaFrames?: number): void; 49 | 50 | /** 51 | * Main rendering. 52 | */ 53 | export function draw(): void; 54 | 55 | /** 56 | * Set camera projection from matrix. 57 | * @param matrixArray An array that is requred 16 elements 58 | */ 59 | export function setProjectionMatrix(matrixArray: Float32Array): void; 60 | 61 | /** 62 | * Set camera projection from perspective parameters. 63 | * @param {number} fov Field of view in degree 64 | * @param {number} aspect Aspect ratio 65 | * @param {number} near Distance of near plane 66 | * @param {number} aspect Distance of far plane 67 | */ 68 | export function setProjectionPerspective(fov: number, aspect: number, near: number, far: number): void; 69 | 70 | /** 71 | * Set camera projection from orthographic parameters. 72 | * @param {number} width Width coordinate of the view plane 73 | * @param {number} height Height coordinate of the view plane 74 | * @param {number} near Distance of near plane 75 | * @param {number} aspect Distance of far plane 76 | */ 77 | export function setProjectionOrthographic(width: number, height: number, near: number, far: number): void; 78 | 79 | /** 80 | * Set camera view from matrix. 81 | * @param matrixArray An array that is requred 16 elements 82 | */ 83 | export function setCameraMatrix(matrixArray: Float32Array): void; 84 | 85 | /** 86 | * Set camera view from lookat parameters. 87 | * @param {number} positionX X value of camera position 88 | * @param {number} positionY Y value of camera position 89 | * @param {number} positionZ Z value of camera position 90 | * @param {number} targetX X value of target position 91 | * @param {number} targetY Y value of target position 92 | * @param {number} targetZ Z value of target position 93 | * @param {number} upvecX X value of upper vector 94 | * @param {number} upvecY Y value of upper vector 95 | * @param {number} upvecZ Z value of upper vector 96 | */ 97 | export function setCameraLookAt( 98 | positionX: number, 99 | positionY: number, 100 | positionZ: number, 101 | targetX: number, 102 | targetY: number, 103 | targetZ: number, 104 | upvecX: number, 105 | upvecY: number, 106 | upvecZ: number 107 | ): void; 108 | 109 | /** 110 | * Set camera view from lookat vector parameters. 111 | * @param {object} position camera position 112 | * @param {object} target target position 113 | * @param {object=} upvec upper vector 114 | */ 115 | export function setCameraLookAtFromVector(position: object, target: object, upvec?: object): void; 116 | 117 | /** 118 | * Load the effect data file (and resources). 119 | * @param {string|ArrayBuffer} data A URL/ArrayBuffer of effect file (*.efk) 120 | * @param {number} scale A magnification rate for the effect. The effect is loaded magnificating with this specified number. 121 | * @param {function=} onload A function that is called at loading complete 122 | * @param {function=} onerror A function that is called at loading error. First argument is a message. Second argument is an url. 123 | * @returns {EffekseerEffect} The effect data 124 | */ 125 | export function loadEffect(data: string|ArrayBuffer, scale?: number, onload?: () => void, onerror?: (reason: string, path: string) => void): EffekseerEffect; 126 | /** 127 | * Load the effect package file (resources included in the package). 128 | * @param {string|ArrayBuffer} data A URL/ArrayBuffer of effect package file (*.efkpkg) 129 | * @param {Object} Unzip An Unzip object. 130 | * @param {number} scale A magnification rate for the effect. The effect is loaded magnificating with this specified number. 131 | * @param {function=} onload A function that is called at loading complete 132 | * @param {function=} onerror A function that is called at loading error. First argument is a message. Second argument is an url. 133 | * @returns {EffekseerEffect} The effect data 134 | */ 135 | export function loadEffectPackage(data: string|ArrayBuffer, Unzip: Object, scale?: number, onload?: () => void, onerror?: (reason: string, path: string) => void): EffekseerEffect; 136 | /** 137 | * Release the specified effect. Don't touch the instance of effect after released. 138 | * @param {EffekseerEffect} effect The loaded effect 139 | */ 140 | export function releaseEffect(effect: EffekseerEffect): void; 141 | 142 | /** 143 | * Play the specified effect. 144 | * @param {EffekseerEffect} effect The loaded effect 145 | * @param {number} x X value of location that is emited 146 | * @param {number} y Y value of location that is emited 147 | * @param {number} z Z value of location that is emited 148 | * @returns {EffekseerHandle} The effect handle 149 | */ 150 | export function play(effect: EffekseerEffect, x: number, y: number, z: number): EffekseerHandle; 151 | 152 | /** 153 | * Stop the all effects. 154 | */ 155 | export function stopAll(): void; 156 | 157 | /** 158 | * Set the resource loader function. 159 | * @param {function} loader 160 | */ 161 | export function setResourceLoader(loader: (path: string, onload?: () => void, onerror?: (reason: string, path: string) => void) => void): void; 162 | 163 | /** 164 | * Get whether VAO is supported 165 | */ 166 | export function isVertexArrayObjectSupported(): boolean; 167 | 168 | export class EffekseerContext { 169 | /** 170 | * Initialize graphics system. 171 | * @param {WebGLRenderingContext} webglContext WebGL Context 172 | * @param {object} settings Some settings with Effekseer initialization 173 | */ 174 | init(webglContext: WebGLRenderingContext, settings?: object): void; 175 | 176 | /** 177 | * Advance frames. 178 | * @param {number=} deltaFrames number of advance frames 179 | */ 180 | update(deltaFrames?: number): void; 181 | 182 | /** 183 | * Main rendering. 184 | */ 185 | draw(): void; 186 | 187 | /** 188 | * Set camera projection from matrix. 189 | * @param matrixArray An array that is requred 16 elements 190 | */ 191 | setProjectionMatrix(matrixArray: Float32Array): void; 192 | 193 | /** 194 | * Set camera projection from perspective parameters. 195 | * @param {number} fov Field of view in degree 196 | * @param {number} aspect Aspect ratio 197 | * @param {number} near Distance of near plane 198 | * @param {number} aspect Distance of far plane 199 | */ 200 | setProjectionPerspective(fov: number, aspect: number, near: number, far: number): void; 201 | 202 | /** 203 | * Set camera projection from orthographic parameters. 204 | * @param {number} width Width coordinate of the view plane 205 | * @param {number} height Height coordinate of the view plane 206 | * @param {number} near Distance of near plane 207 | * @param {number} aspect Distance of far plane 208 | */ 209 | setProjectionOrthographic(width: number, height: number, near: number, far: number): void; 210 | 211 | /** 212 | * Set camera view from matrix. 213 | * @param matrixArray An array that is requred 16 elements 214 | */ 215 | setCameraMatrix(matrixArray: Float32Array): void; 216 | 217 | /** 218 | * Set camera view from lookat parameters. 219 | * @param {number} positionX X value of camera position 220 | * @param {number} positionY Y value of camera position 221 | * @param {number} positionZ Z value of camera position 222 | * @param {number} targetX X value of target position 223 | * @param {number} targetY Y value of target position 224 | * @param {number} targetZ Z value of target position 225 | * @param {number} upvecX X value of upper vector 226 | * @param {number} upvecY Y value of upper vector 227 | * @param {number} upvecZ Z value of upper vector 228 | */ 229 | setCameraLookAt( 230 | positionX: number, 231 | positionY: number, 232 | positionZ: number, 233 | targetX: number, 234 | targetY: number, 235 | targetZ: number, 236 | upvecX: number, 237 | upvecY: number, 238 | upvecZ: number 239 | ): void; 240 | 241 | /** 242 | * Set camera view from lookat vector parameters. 243 | * @param {object} position camera position 244 | * @param {object} target target position 245 | * @param {object=} upvec upper vector 246 | */ 247 | setCameraLookAtFromVector(position: object, target: object, upvec?: object): void; 248 | 249 | /** 250 | * Load the effect data file (and resources). 251 | * @param {string|ArrayBuffer} data A URL/ArrayBuffer of effect file (*.efk) 252 | * @param {number} scale A magnification rate for the effect. The effect is loaded magnificating with this specified number. 253 | * @param {function=} onload A function that is called at loading complete 254 | * @param {function=} onerror A function that is called at loading error. First argument is a message. Second argument is an url. 255 | * @param {function=} redirect A function to redirect a path. First argument is an url and return redirected url. 256 | * @returns {EffekseerEffect} The effect data 257 | */ 258 | loadEffect(data: string|ArrayBuffer, scale?: number, onload?: () => void, onerror?: (reason: string, path: string) => void, redirect?: (path: string) => string): EffekseerEffect; 259 | 260 | /** 261 | * Load the effect package file (resources included in the package). 262 | * @param {string|ArrayBuffer} data A URL/ArrayBuffer of effect file (*.efk) 263 | * @param {Object} Unzip An Unzip object. 264 | * @param {number} scale A magnification rate for the effect. The effect is loaded magnificating with this specified number. 265 | * @param {function=} onload A function that is called at loading complete 266 | * @param {function=} onerror A function that is called at loading error. First argument is a message. Second argument is an url. 267 | * @returns {EffekseerEffect} The effect data 268 | */ 269 | loadEffectPackage(data: string|ArrayBuffer, Unzip: Object, scale?: number, onload?: () => void, onerror?: (reason: string, path: string) => void): EffekseerEffect; 270 | 271 | /** 272 | * Release the specified effect. Don't touch the instance of effect after released. 273 | * @param {EffekseerEffect} effect The loaded effect 274 | */ 275 | releaseEffect(effect: EffekseerEffect): void; 276 | 277 | /** 278 | * Play the specified effect. 279 | * @param {EffekseerEffect} effect The loaded effect 280 | * @param {number} x X value of location that is emited 281 | * @param {number} y Y value of location that is emited 282 | * @param {number} z Z value of location that is emited 283 | * @returns {EffekseerHandle} The effect handle 284 | */ 285 | play(effect: EffekseerEffect, x: number, y: number, z: number): EffekseerHandle; 286 | 287 | /** 288 | * Stop the all effects. 289 | */ 290 | stopAll(): void; 291 | 292 | /** 293 | * Set the resource loader function. 294 | * @param {function} loader 295 | */ 296 | setResourceLoader(loader: (path: string, onload?: () => void, onerror?: (reason: string, path: string) => void) => void): void; 297 | 298 | /** 299 | * Get whether VAO is supported 300 | */ 301 | isVertexArrayObjectSupported(): boolean; 302 | 303 | /** 304 | * Gets the number of remaining allocated instances. 305 | */ 306 | getRestInstancesCount(): Number; 307 | 308 | /** 309 | * Gets a time when updating 310 | */ 311 | getUpdateTime(): Number; 312 | 313 | /** 314 | * Gets a time when drawing 315 | */ 316 | getDrawTime(): Number; 317 | 318 | /** 319 | * Set the flag whether the library restores OpenGL states 320 | * if specified true, it makes slow. 321 | * if specified false, You need to restore OpenGL states by yourself 322 | * it must be called after init 323 | * @param {boolean} flag 324 | */ 325 | setRestorationOfStatesFlag(flag: boolean): void; 326 | 327 | /** 328 | * Capture current frame buffer and set the image as a background 329 | * @param {number} x captured image's x offset 330 | * @param {number} y captured image's y offset 331 | * @param {number} width captured image's width 332 | * @param {number} height captured image's height 333 | */ 334 | captureBackground(x: number, y: number, width: number, height: number): void; 335 | 336 | /** 337 | * Reset background 338 | */ 339 | resetBackground(): void; 340 | } 341 | 342 | export class EffekseerEffect { 343 | constructor(); 344 | } 345 | 346 | export class EffekseerHandle { 347 | constructor(native: any); 348 | 349 | /** 350 | * Stop this effect instance. 351 | */ 352 | stop(): void; 353 | 354 | /** 355 | * Stop the root node of this effect instance. 356 | */ 357 | stopRoot(): void; 358 | 359 | /** 360 | * if returned false, this effect is end of playing. 361 | */ 362 | readonly exists: boolean; 363 | 364 | /** 365 | * Set frame of this effect instance. 366 | * @param {number} frame Frame of this effect instance. 367 | */ 368 | setFrame(frame: number): void; 369 | 370 | /** 371 | * Set the location of this effect instance. 372 | * @param {number} x X value of location 373 | * @param {number} y Y value of location 374 | * @param {number} z Z value of location 375 | */ 376 | setLocation(x: number, y: number, z: number): void; 377 | /** 378 | * Set the rotation of this effect instance. 379 | * @param {number} x X value of euler angle 380 | * @param {number} y Y value of euler angle 381 | * @param {number} z Z value of euler angle 382 | */ 383 | setRotation(x: number, y: number, z: number): void; 384 | 385 | /** 386 | * Set the scale of this effect instance. 387 | * @param {number} x X value of scale factor 388 | * @param {number} y Y value of scale factor 389 | * @param {number} z Z value of scale factor 390 | */ 391 | setScale(x: number, y: number, z: number): void; 392 | 393 | /** 394 | * Set the model matrix of this effect instance. 395 | * @param {array} matrixArray An array that is requred 16 elements 396 | */ 397 | setMatrix(matrixArray: Float32Array): void; 398 | 399 | /** 400 | * Set the color of this effect instance. 401 | * @param {number} r R channel value of color 402 | * @param {number} g G channel value of color 403 | * @param {number} b B channel value of color 404 | * @param {number} a A channel value of color 405 | */ 406 | setAllColor(r: number, g: number, b: number, a: number): void; 407 | 408 | /** 409 | * Set the target location of this effect instance. 410 | * @param {number} x X value of target location 411 | * @param {number} y Y value of target location 412 | * @param {number} z Z value of target location 413 | */ 414 | setTargetLocation(x: number, y: number, z: number): void; 415 | 416 | /** 417 | * get a dynamic parameter, which changes effect parameters dynamically while playing 418 | * @param {number} index slot index 419 | * @returns {number} value 420 | */ 421 | getDynamicInput(index: number): number; 422 | 423 | /** 424 | * specfiy a dynamic parameter, which changes effect parameters dynamically while playing 425 | * @param {number} index slot index 426 | * @param {number} value value 427 | */ 428 | setDynamicInput(index: number, value: number): void; 429 | 430 | /** 431 | * Sends the specified trigger to the currently playing effect 432 | * @param {number} index trigger index 433 | */ 434 | sendTrigger(index: number): void; 435 | 436 | /** 437 | * Set the paused flag of this effect instance. 438 | * if specified true, this effect playing will not advance. 439 | * @param {boolean} paused Paused flag 440 | */ 441 | setPaused(paused: boolean): void; 442 | 443 | /** 444 | * Set the shown flag of this effect instance. 445 | * if specified false, this effect will be invisible. 446 | * @param {boolean} shown Shown flag 447 | */ 448 | setShown(shown: boolean): void; 449 | /** 450 | * Set playing speed of this effect. 451 | * @param {number} speed Speed ratio 452 | */ 453 | setSpeed(speed: number): void; 454 | /** 455 | * Set random seed of this effect. 456 | * @param {number} seed random seed 457 | */ 458 | setRandomSeed(seed: number): void; 459 | } 460 | } 461 | 462 | declare module "effekseer" { 463 | export = effekseer; 464 | } --------------------------------------------------------------------------------