├── .codesandbox └── ci.json ├── .devcontainer └── devcontainer.json ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ ├── ---feature-request.md │ └── --support-question.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── Setup.ts ├── index.css ├── main.ts ├── manager.ts ├── preview.ts ├── public │ ├── .gitkeep │ ├── dancing_hall_1k.exr │ ├── paper_normal.jpg │ ├── photo-1678043639454-dbdf877f0ae8.jpeg │ ├── rgh.jpg │ ├── round_platform_1k.exr │ └── sprites │ │ ├── alien.png │ │ ├── boy_hash.json │ │ ├── boy_hash.png │ │ ├── flame.json │ │ └── flame.png ├── stories │ ├── AccumulativeShadows.stories.ts │ ├── Billboard.stories.ts │ ├── Caustics.stories.ts │ ├── Clouds.stories.ts │ ├── Grid.stories.ts │ ├── MeshDistortMaterial.stories.ts │ ├── MeshPortalMaterial.stories.ts │ ├── MeshPortalMaterialSdf.stories.ts │ ├── MeshReflectorMaterial.stories.ts │ ├── MeshTransmissionMaterial.stories.ts │ ├── MeshWobbleMaterial.stories.ts │ ├── Outlines.stories.ts │ ├── PCSS.stories.ts │ ├── Splat.stories.ts │ ├── SpriteAnimator.stories.ts │ ├── shaderMaterial.stories.ts │ └── volumetricSpotlight.stories.ts └── theme.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── logo.jpg ├── package.json ├── release.config.js ├── rollup.config.js ├── src ├── core │ ├── AccumulativeShadows.ts │ ├── Billboard.ts │ ├── Caustics.ts │ ├── Cloud.ts │ ├── Grid.ts │ ├── MeshPortalMaterial.ts │ ├── Outlines.ts │ ├── Splat.ts │ ├── SpriteAnimator.ts │ ├── index.ts │ ├── pcss.ts │ ├── shaderMaterial.ts │ └── useFBO.ts ├── helpers │ ├── Pass.ts │ ├── constants.ts │ ├── deprecated.ts │ └── glsl │ │ └── distort.vert.glsl ├── index.ts └── materials │ ├── BlurPass.ts │ ├── ConvolutionMaterial.ts │ ├── MeshDiscardMaterial.ts │ ├── MeshDistortMaterial.ts │ ├── MeshReflectorMaterial.ts │ ├── MeshTransmissionMaterial.ts │ ├── MeshWobbleMaterial.ts │ ├── SpotLightMaterial.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["4nc1u", "bfplr", "1wh6f"], 3 | "packages": ["dist"], 4 | "node": "18" 5 | } 6 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:18", 3 | "hostRequirements": { 4 | "memory": "8gb" 5 | }, 6 | "waitFor": "onCreateCommand", 7 | "updateContentCommand": "yarn install", 8 | "postCreateCommand": "", 9 | "postAttachCommand": "yarn storybook -- --port 6006", 10 | "customizations": { 11 | "codespaces": { 12 | "openFiles": ["CONTRIBUTING.md"] 13 | }, 14 | "vscode": { 15 | "settings": { 16 | "editor.formatOnSave": true 17 | }, 18 | "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 19 | } 20 | }, 21 | "portsAttributes": { 22 | "6006": { 23 | "label": "Storybook server", 24 | "onAutoForward": "openPreview" 25 | } 26 | }, 27 | "forwardPorts": [6006] 28 | } 29 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .storybook/public/draco-gltf/ 2 | dist/ 3 | node_modules/ 4 | storybook-static/ 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "shared-node-browser": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "prettier", 10 | "plugin:prettier/recommended", 11 | "plugin:import/errors", 12 | "plugin:import/warnings" 13 | ], 14 | "plugins": ["@typescript-eslint", "prettier", "import"], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "ecmaVersion": 2018, 19 | "jsx": true 20 | }, 21 | "sourceType": "module" 22 | }, 23 | "rules": { 24 | "curly": ["error", "multi-line", "consistent"], 25 | "no-console": "off", 26 | "no-empty-pattern": "error", 27 | "no-duplicate-imports": "error", 28 | "prefer-const": "error", 29 | "import/no-unresolved": ["error", { "commonjs": true, "amd": true }], 30 | "import/export": "error", 31 | "import/named": "off", 32 | "import/no-named-as-default": "off", 33 | "import/no-named-as-default-member": "off", 34 | "import/namespace": "off", 35 | "import/default": "off", 36 | "@typescript-eslint/explicit-module-boundary-types": "off", 37 | "no-unused-vars": ["off"], 38 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], 39 | "@typescript-eslint/no-use-before-define": "off", 40 | "@typescript-eslint/no-empty-function": "off", 41 | "@typescript-eslint/no-empty-interface": "off", 42 | "@typescript-eslint/no-explicit-any": "off" 43 | }, 44 | "settings": { 45 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"], 46 | "import/parsers": { 47 | "@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx"] 48 | }, 49 | "import/resolver": { 50 | "node": { 51 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], 52 | "paths": ["src"] 53 | } 54 | } 55 | }, 56 | "overrides": [ 57 | { 58 | "files": ["src"], 59 | "parserOptions": { 60 | "project": ["./tsconfig.json", "./storybook/tsconfig.json"] 61 | } 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: "Bugs, missing documentation, or unexpected behavior \U0001F914." 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | 18 | 19 | - `three` version: 20 | - `@pmndrs/vanilla` version: 21 | - `node` version: 22 | - `npm` (or `yarn`) version: 23 | 24 | ### Problem description: 25 | 26 | 27 | 28 | ### Relevant code: 29 | 30 | 31 | 32 | ```js 33 | let your = (code, tell) => `the ${story}` 34 | ``` 35 | 36 | ### Suggested solution: 37 | 38 | 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature Request" 3 | about: "I have a suggestion (and might want to implement myself \U0001F642)!" 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the feature you'd like: 10 | 11 | 15 | 16 | ### Suggested implementation: 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--support-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '❓ Support Question' 3 | about: "I have a question \U0001F4AC" 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | --- 8 | 9 | 19 | 20 | ### What is your question: 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ### Why 12 | 13 | 14 | 15 | ### What 16 | 17 | 18 | 19 | ### Checklist 20 | 21 | 22 | 23 | 27 | 28 | - [ ] Documentation updated 29 | - [ ] Ready to be merged 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | 7 | # Cancel any previous run (see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency) 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | release-job: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: '18' 20 | cache: 'yarn' 21 | - id: main 22 | run: | 23 | yarn install 24 | yarn build-storybook 25 | yarn release 26 | env: 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | - uses: actions/upload-pages-artifact@v3 30 | with: 31 | path: ./storybook-static 32 | 33 | # See: https://github.com/actions/deploy-pages 34 | deploy-job: 35 | needs: release-job 36 | permissions: 37 | pages: write 38 | id-token: write 39 | environment: 40 | name: github-pages 41 | url: ${{ steps.deployment.outputs.page_url }} 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Deploy to GitHub Pages 45 | id: deployment 46 | uses: actions/deploy-pages@v4 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | build/ 5 | types/ 6 | storybook-static/ 7 | Thumbs.db 8 | ehthumbs.db 9 | Desktop.ini 10 | $RECYCLE.BIN/ 11 | .DS_Store 12 | .vscode 13 | .docz/ 14 | package-lock.json 15 | coverage/ 16 | .idea 17 | yarn-error.log 18 | .size-snapshot.json 19 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install pretty-quick --staged 5 | npm run eslint:ci --quiet 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .storybook 2 | storybook-static 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | storybook-static/ 3 | *.typeface.json 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "printWidth": 120, 7 | "useTabs": false, 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/Setup.ts: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera, Scene, WebGLRenderer, ACESFilmicToneMapping } from 'three' 2 | import { addons } from '@storybook/addons' 3 | import { STORY_CHANGED } from '@storybook/core-events' 4 | 5 | const channel = addons.getChannel() 6 | 7 | const storyListener = () => { 8 | console.log('custom force reload') 9 | location.reload() 10 | } 11 | 12 | const setupStoryListener = () => { 13 | channel.removeListener(STORY_CHANGED, storyListener) 14 | channel.addListener(STORY_CHANGED, storyListener) 15 | } 16 | 17 | setupStoryListener() 18 | 19 | declare global { 20 | interface Window { 21 | root: HTMLDivElement 22 | canvas: HTMLCanvasElement 23 | context: WebGL2RenderingContext 24 | } 25 | const root: HTMLDivElement 26 | const canvas: HTMLCanvasElement 27 | const context: WebGL2RenderingContext 28 | } 29 | 30 | window.canvas = root.appendChild(document.createElement('canvas')) 31 | window.context = canvas.getContext('webgl2')! 32 | window.canvas.style.display = 'block' 33 | 34 | export const Setup = () => { 35 | const renderer = new WebGLRenderer({ alpha: true, canvas, context }) 36 | renderer.toneMapping = ACESFilmicToneMapping 37 | const camera = new PerspectiveCamera(45, 1, 1, 1000) 38 | camera.position.z = 3 39 | 40 | const scene = new Scene() 41 | 42 | const resize = () => { 43 | renderer.setPixelRatio(Math.min(2, Math.max(1, window.devicePixelRatio))) 44 | renderer.setSize(root.clientWidth, root.clientHeight) 45 | camera.aspect = root.clientWidth / root.clientHeight 46 | camera.updateProjectionMatrix() 47 | } 48 | resize() 49 | window.addEventListener('resize', resize) 50 | 51 | let toRender: XRFrameRequestCallback | undefined 52 | renderer.setAnimationLoop((...args) => { 53 | toRender?.(...args) 54 | renderer.render(scene, camera) 55 | }) 56 | const render = (callback: XRFrameRequestCallback) => void (toRender = callback) 57 | 58 | return { renderer, camera, scene, render } 59 | } 60 | -------------------------------------------------------------------------------- /.storybook/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root, 4 | canvas { 5 | width: 100%; 6 | height: 100%; 7 | margin: 0; 8 | padding: 0; 9 | -webkit-touch-callout: none; 10 | -webkit-user-select: none; 11 | -khtml-user-select: none; 12 | -moz-user-select: none; 13 | -ms-user-select: none; 14 | user-select: none; 15 | overflow: hidden; 16 | background-color: #121212; 17 | } 18 | 19 | #root { 20 | overflow: auto; 21 | } 22 | 23 | .html-story-block { 24 | color: white; 25 | width: 120px; 26 | position: relative; 27 | margin-left: 100px; 28 | } 29 | 30 | .html-story-block.margin300 { 31 | margin-left: 300px; 32 | } 33 | 34 | .html-story-block:before { 35 | content: ''; 36 | display: block; 37 | position: absolute; 38 | 39 | top: 50%; 40 | left: -60px; 41 | 42 | width: 60px; 43 | height: 1px; 44 | background-color: white; 45 | } 46 | 47 | .html-story-label { 48 | background-color: white; 49 | color: black; 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | padding: 8px; 54 | } 55 | 56 | .html-story-label-B { 57 | font-size: 50px; 58 | } 59 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | 3 | export default { 4 | staticDirs: ['./public'], 5 | core: { 6 | builder: '@storybook/builder-webpack5', 7 | }, 8 | stories: ['./stories/**/*.stories.{ts,tsx}'], 9 | addons: [], 10 | typescript: { 11 | check: true, 12 | }, 13 | // https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-implicit-postcss-loader 14 | features: { 15 | postcss: false, 16 | }, 17 | webpackFinal: (config) => { 18 | config.module.rules.push({ 19 | test: /\.(glsl|vs|fs|vert|frag)$/, 20 | exclude: /node_modules/, 21 | use: ['raw-loader', 'glslify-loader'], 22 | include: resolve(__dirname, '../'), 23 | }) 24 | 25 | return config 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons' 2 | import theme from './theme' 3 | 4 | addons.setConfig({ 5 | theme, 6 | panelPosition: 'right', 7 | showPanel: true, 8 | }) 9 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import { ColorManagement, Scene } from 'three' 2 | import './index.css' 3 | 4 | export const parameters = { 5 | layout: 'fullscreen', 6 | } 7 | 8 | if ('enabled' in ColorManagement) (ColorManagement as any).enabled = true 9 | else ColorManagement.legacyMode = false 10 | 11 | const pool: THREE.Object3D[] = [] 12 | 13 | const add = Scene.prototype.add 14 | Scene.prototype.add = function (...objects) { 15 | pool.push(...objects) 16 | return add.apply(this, objects) 17 | } 18 | 19 | export const decorators = [ 20 | (Story) => { 21 | while (pool.length) { 22 | const object = pool.shift() 23 | if (object?.parent) { 24 | object.traverse((node) => { 25 | ;(node as any).dispose?.() 26 | for (const key in node) node[key]?.dispose?.() 27 | }) 28 | } 29 | } 30 | 31 | Story() 32 | 33 | // @ts-ignore 34 | return window.canvas 35 | }, 36 | ] 37 | -------------------------------------------------------------------------------- /.storybook/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/drei-vanilla/fb0765741897c70f13589950495917fa4f5d15a0/.storybook/public/.gitkeep -------------------------------------------------------------------------------- /.storybook/public/dancing_hall_1k.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/drei-vanilla/fb0765741897c70f13589950495917fa4f5d15a0/.storybook/public/dancing_hall_1k.exr -------------------------------------------------------------------------------- /.storybook/public/paper_normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/drei-vanilla/fb0765741897c70f13589950495917fa4f5d15a0/.storybook/public/paper_normal.jpg -------------------------------------------------------------------------------- /.storybook/public/photo-1678043639454-dbdf877f0ae8.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/drei-vanilla/fb0765741897c70f13589950495917fa4f5d15a0/.storybook/public/photo-1678043639454-dbdf877f0ae8.jpeg -------------------------------------------------------------------------------- /.storybook/public/rgh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/drei-vanilla/fb0765741897c70f13589950495917fa4f5d15a0/.storybook/public/rgh.jpg -------------------------------------------------------------------------------- /.storybook/public/round_platform_1k.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/drei-vanilla/fb0765741897c70f13589950495917fa4f5d15a0/.storybook/public/round_platform_1k.exr -------------------------------------------------------------------------------- /.storybook/public/sprites/alien.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/drei-vanilla/fb0765741897c70f13589950495917fa4f5d15a0/.storybook/public/sprites/alien.png -------------------------------------------------------------------------------- /.storybook/public/sprites/boy_hash.json: -------------------------------------------------------------------------------- 1 | { 2 | "frames": { 3 | "Celebration_000": { 4 | "frame": { "x": 1, "y": 1, "w": 560, "h": 440 }, 5 | "rotated": false, 6 | "trimmed": false, 7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 8 | "sourceSize": { "w": 560, "h": 440 } 9 | }, 10 | "Celebration_001": { 11 | "frame": { "x": 1, "y": 443, "w": 560, "h": 440 }, 12 | "rotated": false, 13 | "trimmed": false, 14 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 15 | "sourceSize": { "w": 560, "h": 440 } 16 | }, 17 | "Celebration_002": { 18 | "frame": { "x": 1, "y": 885, "w": 560, "h": 440 }, 19 | "rotated": false, 20 | "trimmed": false, 21 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 22 | "sourceSize": { "w": 560, "h": 440 } 23 | }, 24 | "Celebration_003": { 25 | "frame": { "x": 1, "y": 1327, "w": 560, "h": 440 }, 26 | "rotated": false, 27 | "trimmed": false, 28 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 29 | "sourceSize": { "w": 560, "h": 440 } 30 | }, 31 | "Celebration_004": { 32 | "frame": { "x": 563, "y": 1, "w": 560, "h": 440 }, 33 | "rotated": false, 34 | "trimmed": false, 35 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 36 | "sourceSize": { "w": 560, "h": 440 } 37 | }, 38 | "Celebration_005": { 39 | "frame": { "x": 563, "y": 443, "w": 560, "h": 440 }, 40 | "rotated": false, 41 | "trimmed": false, 42 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 43 | "sourceSize": { "w": 560, "h": 440 } 44 | }, 45 | "Celebration_006": { 46 | "frame": { "x": 563, "y": 885, "w": 560, "h": 440 }, 47 | "rotated": false, 48 | "trimmed": false, 49 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 50 | "sourceSize": { "w": 560, "h": 440 } 51 | }, 52 | "Celebration_007": { 53 | "frame": { "x": 563, "y": 1327, "w": 560, "h": 440 }, 54 | "rotated": false, 55 | "trimmed": false, 56 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 57 | "sourceSize": { "w": 560, "h": 440 } 58 | }, 59 | "Celebration_008": { 60 | "frame": { "x": 1125, "y": 1, "w": 560, "h": 440 }, 61 | "rotated": false, 62 | "trimmed": false, 63 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 64 | "sourceSize": { "w": 560, "h": 440 } 65 | }, 66 | "Celebration_009": { 67 | "frame": { "x": 1125, "y": 443, "w": 560, "h": 440 }, 68 | "rotated": false, 69 | "trimmed": false, 70 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 71 | "sourceSize": { "w": 560, "h": 440 } 72 | }, 73 | "Celebration_010": { 74 | "frame": { "x": 1125, "y": 885, "w": 560, "h": 440 }, 75 | "rotated": false, 76 | "trimmed": false, 77 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 78 | "sourceSize": { "w": 560, "h": 440 } 79 | }, 80 | "Celebration_011": { 81 | "frame": { "x": 1125, "y": 1327, "w": 560, "h": 440 }, 82 | "rotated": false, 83 | "trimmed": false, 84 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 85 | "sourceSize": { "w": 560, "h": 440 } 86 | }, 87 | "Idle_000": { 88 | "frame": { "x": 1687, "y": 1, "w": 560, "h": 440 }, 89 | "rotated": false, 90 | "trimmed": false, 91 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 92 | "sourceSize": { "w": 560, "h": 440 } 93 | }, 94 | "Idle_001": { 95 | "frame": { "x": 2249, "y": 1, "w": 560, "h": 440 }, 96 | "rotated": false, 97 | "trimmed": false, 98 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 99 | "sourceSize": { "w": 560, "h": 440 } 100 | }, 101 | "Idle_002": { 102 | "frame": { "x": 2811, "y": 1, "w": 560, "h": 440 }, 103 | "rotated": false, 104 | "trimmed": false, 105 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 106 | "sourceSize": { "w": 560, "h": 440 } 107 | }, 108 | "Idle_003": { 109 | "frame": { "x": 1687, "y": 443, "w": 560, "h": 440 }, 110 | "rotated": false, 111 | "trimmed": false, 112 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 113 | "sourceSize": { "w": 560, "h": 440 } 114 | }, 115 | "Idle_004": { 116 | "frame": { "x": 1687, "y": 885, "w": 560, "h": 440 }, 117 | "rotated": false, 118 | "trimmed": false, 119 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 120 | "sourceSize": { "w": 560, "h": 440 } 121 | }, 122 | "Idle_005": { 123 | "frame": { "x": 1687, "y": 1327, "w": 560, "h": 440 }, 124 | "rotated": false, 125 | "trimmed": false, 126 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 127 | "sourceSize": { "w": 560, "h": 440 } 128 | }, 129 | "Idle_006": { 130 | "frame": { "x": 2249, "y": 443, "w": 560, "h": 440 }, 131 | "rotated": false, 132 | "trimmed": false, 133 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 134 | "sourceSize": { "w": 560, "h": 440 } 135 | }, 136 | "Idle_007": { 137 | "frame": { "x": 2811, "y": 443, "w": 560, "h": 440 }, 138 | "rotated": false, 139 | "trimmed": false, 140 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 141 | "sourceSize": { "w": 560, "h": 440 } 142 | }, 143 | "Idle_008": { 144 | "frame": { "x": 2249, "y": 885, "w": 560, "h": 440 }, 145 | "rotated": false, 146 | "trimmed": false, 147 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 148 | "sourceSize": { "w": 560, "h": 440 } 149 | }, 150 | "Idle_009": { 151 | "frame": { "x": 2249, "y": 1327, "w": 560, "h": 440 }, 152 | "rotated": false, 153 | "trimmed": false, 154 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 155 | "sourceSize": { "w": 560, "h": 440 } 156 | }, 157 | "Idle_010": { 158 | "frame": { "x": 2811, "y": 885, "w": 560, "h": 440 }, 159 | "rotated": false, 160 | "trimmed": false, 161 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 162 | "sourceSize": { "w": 560, "h": 440 } 163 | }, 164 | "Idle_011": { 165 | "frame": { "x": 2811, "y": 1327, "w": 560, "h": 440 }, 166 | "rotated": false, 167 | "trimmed": false, 168 | "spriteSourceSize": { "x": 0, "y": 0, "w": 560, "h": 440 }, 169 | "sourceSize": { "w": 560, "h": 440 } 170 | } 171 | }, 172 | "meta": { 173 | "app": "https://www.codeandweb.com/texturepacker", 174 | "version": "1.0", 175 | "image": "boy_hash.png", 176 | "format": "RGBA8888", 177 | "size": { "w": 3372, "h": 1768 }, 178 | "scale": "1", 179 | "smartupdate": "$TexturePacker:SmartUpdate:d301d951d8abae024067a0aa4f967bb8:80aa58e215269ef29d30b35e5647e226:19277069a4baafe8c3985fd737a4d234$" 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /.storybook/public/sprites/boy_hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/drei-vanilla/fb0765741897c70f13589950495917fa4f5d15a0/.storybook/public/sprites/boy_hash.png -------------------------------------------------------------------------------- /.storybook/public/sprites/flame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/drei-vanilla/fb0765741897c70f13589950495917fa4f5d15a0/.storybook/public/sprites/flame.png -------------------------------------------------------------------------------- /.storybook/stories/AccumulativeShadows.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import GUI from 'lil-gui' 4 | import { Meta } from '@storybook/html' 5 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js' 6 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 7 | 8 | import { ProgressiveLightMap, SoftShadowMaterial } from '../../src/core/AccumulativeShadows' 9 | 10 | export default { 11 | title: 'Shaders/AccumulativeShadows', 12 | } as Meta 13 | 14 | let gui: GUI, 15 | scene: THREE.Scene, 16 | camera: THREE.PerspectiveCamera, 17 | renderer: THREE.WebGLRenderer, 18 | animateLoop: (arg0: (time: number) => void) => void 19 | 20 | let plm: ProgressiveLightMap, // class handles the shadow accumulation part 21 | gLights: THREE.Group, // group containing all the random lights 22 | gPlane: THREE.Mesh, // shadow catching plane 23 | shadowMaterial: InstanceType // instance of SoftShadowMaterial material applied to plane to make only the shadows visible 24 | 25 | const shadowParams = { 26 | /** Temporal accumulates shadows over time which is more performant but has a visual regression over instant results, false */ 27 | temporal: true, 28 | 29 | /** How many frames it can render, more yields cleaner results but takes more time, 40 */ 30 | frames: 40, 31 | 32 | /** Can limit the amount of frames rendered if frames === Infinity, usually to get some performance back once a movable scene has settled, Infinity */ 33 | limit: Infinity, 34 | 35 | /** If frames === Infinity blend controls the refresh ratio, 100 */ 36 | blend: 40, 37 | 38 | /** Scale of the plane, */ 39 | scale: 10, 40 | 41 | /** Opacity of the plane, 1 */ 42 | opacity: 0.8, 43 | 44 | /** Discards alpha pixels, 0.65 */ 45 | alphaTest: 0.75, 46 | 47 | /** ColorBlend, how much colors turn to black, 0 is black, 2 */ 48 | colorBlend: 2, 49 | } 50 | 51 | /** 52 | * Shadow properties & common dir light properties 53 | */ 54 | const lightParams = { 55 | /** Light position */ 56 | position: new THREE.Vector3(3, 5, 3), 57 | 58 | /** Radius of the jiggle, higher values make softer light */ 59 | radius: 1, 60 | 61 | /** Amount of lights*/ 62 | amount: 8, 63 | 64 | /** Light intensity */ 65 | intensity: parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 155 ? Math.PI : 1, 66 | 67 | /** Ambient occlusion, lower values mean less AO, hight more, you can mix AO and directional light */ 68 | ambient: 0.5, 69 | 70 | bias: 0.001, //shadow bias 71 | mapSize: 1024, // shadow map res 72 | size: 8, // shadow camera top,bottom,left,right 73 | near: 0.5, // shadow camera near 74 | far: 200, // shadow camera far 75 | } 76 | 77 | /** 78 | * keeping track of shadow render progress 79 | */ 80 | const api = { 81 | count: 0, 82 | } 83 | 84 | export const AccShadowStory = async () => { 85 | const setupResult = Setup() 86 | scene = setupResult.scene 87 | camera = setupResult.camera 88 | renderer = setupResult.renderer 89 | animateLoop = setupResult.render 90 | 91 | gui = new GUI({ title: AccShadowStory.storyName }) 92 | renderer.shadowMap.enabled = true 93 | renderer.toneMapping = THREE.ACESFilmicToneMapping 94 | camera.position.set(2, 3, 4) 95 | 96 | const controls = new OrbitControls(camera, renderer.domElement) 97 | controls.update() 98 | 99 | setupEnvironment() 100 | 101 | // setup accumulative shadows 102 | setupAccumulativeShadows() 103 | 104 | // add some basic models which cast shadows 105 | 106 | // sphere 107 | const sphere = new THREE.Mesh( 108 | new THREE.SphereGeometry(0.5).translate(0, 0.5, 0), 109 | new THREE.MeshStandardMaterial({ color: 0xffffff * Math.random(), roughness: 0, metalness: 1 }) 110 | ) 111 | sphere.name = 'sphere' 112 | sphere.castShadow = true 113 | sphere.receiveShadow = true 114 | sphere.position.set(2, 0, -1.5) 115 | scene.add(sphere) 116 | 117 | // cube 118 | const cube = new THREE.Mesh( 119 | new THREE.CylinderGeometry(0.5, 0.5, 1).translate(0, 0.5, 0), 120 | new THREE.MeshStandardMaterial({ color: 0xffffff * Math.random(), roughness: 0.3, metalness: 0 }) 121 | ) 122 | cube.name = 'cube' 123 | cube.castShadow = true 124 | cube.receiveShadow = true 125 | cube.position.set(-1.5, 0, 1.5) 126 | scene.add(cube) 127 | 128 | // torus 129 | const torus = new THREE.Mesh( 130 | new THREE.TorusKnotGeometry(0.5, 0.2, 80, 64).translate(0, 0.9, 0), 131 | new THREE.MeshStandardMaterial({ color: 0xffffff * Math.random(), roughness: 0.3, metalness: 0 }) 132 | ) 133 | torus.name = 'torus' 134 | torus.castShadow = true 135 | torus.receiveShadow = true 136 | torus.position.set(0, 0, 0) 137 | scene.add(torus) 138 | 139 | // apart from clearing the shadows ,this also traverses the scene and finds which objects need to cast shadows 140 | // so call it once all the objects are loaded 141 | plm.clear() 142 | } 143 | 144 | /** 145 | * Setup HDRI and background 146 | */ 147 | const setupEnvironment = () => { 148 | const exrLoader = new EXRLoader() 149 | scene.background = new THREE.Color('grey') 150 | gui.addColor(scene, 'background') 151 | 152 | // exr from polyhaven.com 153 | exrLoader.load('dancing_hall_1k.exr', (exrTex) => { 154 | exrTex.mapping = THREE.EquirectangularReflectionMapping 155 | scene.environment = exrTex 156 | }) 157 | } 158 | 159 | /** 160 | * Setup shadow catching plane 161 | */ 162 | const setupAccumulativeShadows = () => { 163 | plm = new ProgressiveLightMap(renderer, scene, 1024) 164 | 165 | // Material applied to the shadow-catching plane 166 | shadowMaterial = new SoftShadowMaterial({ 167 | map: plm.progressiveLightMap2.texture, // Ignore TypeScript error, this texture contains the rendered shadow image 168 | transparent: true, 169 | depthWrite: false, 170 | toneMapped: true, 171 | blend: shadowParams.colorBlend, // Color blend 172 | alphaTest: 0, // Set to 0 so that the first frame, where nothing is computed, does not show a black plane 173 | }) 174 | 175 | // shadow catching plane 176 | gPlane = new THREE.Mesh(new THREE.PlaneGeometry(1, 1).rotateX(-Math.PI / 2), shadowMaterial) 177 | gPlane.scale.setScalar(shadowParams.scale) 178 | gPlane.receiveShadow = true 179 | scene.add(gPlane) 180 | 181 | // connect plane to ProgressiveLightMap class 182 | plm.configure(gPlane) 183 | 184 | // create group to hold the lights 185 | gLights = new THREE.Group() 186 | 187 | // create directional lights to speed up the convergence 188 | for (let l = 0; l < lightParams.amount; l++) { 189 | const dirLight = new THREE.DirectionalLight(0xffffff, lightParams.intensity / lightParams.amount) 190 | dirLight.name = 'dir_light_' + l 191 | dirLight.castShadow = true 192 | dirLight.shadow.bias = lightParams.bias 193 | dirLight.shadow.camera.near = lightParams.near 194 | dirLight.shadow.camera.far = lightParams.far 195 | dirLight.shadow.camera.right = lightParams.size / 2 196 | dirLight.shadow.camera.left = -lightParams.size / 2 197 | dirLight.shadow.camera.top = lightParams.size / 2 198 | dirLight.shadow.camera.bottom = -lightParams.size / 2 199 | dirLight.shadow.mapSize.width = lightParams.mapSize 200 | dirLight.shadow.mapSize.height = lightParams.mapSize 201 | gLights.add(dirLight) 202 | } 203 | 204 | // add lil-gui parameters 205 | addPlmGui(gui) 206 | 207 | // request animation frame 208 | animateLoop(() => { 209 | temporalUpdate() // add this to raf loop 210 | }) 211 | } 212 | 213 | /** 214 | * Scramble the light positions to form creamy convergence 215 | */ 216 | function randomiseLightPositions() { 217 | const vLength = lightParams.position.length() 218 | 219 | for (let i = 0; i < gLights.children.length; i++) { 220 | const light = gLights.children[i] 221 | if (Math.random() > lightParams.ambient) { 222 | light.position.set( 223 | lightParams.position.x + THREE.MathUtils.randFloatSpread(lightParams.radius), 224 | lightParams.position.y + THREE.MathUtils.randFloatSpread(lightParams.radius), 225 | lightParams.position.z + THREE.MathUtils.randFloatSpread(lightParams.radius) 226 | ) 227 | } else { 228 | let lambda = Math.acos(2 * Math.random() - 1) - Math.PI / 2.0 229 | let phi = 2 * Math.PI * Math.random() 230 | light.position.set( 231 | Math.cos(lambda) * Math.cos(phi) * vLength, 232 | Math.abs(Math.cos(lambda) * Math.sin(phi) * vLength), 233 | Math.sin(lambda) * vLength 234 | ) 235 | } 236 | } 237 | } 238 | 239 | /** 240 | * Clears the shadows, sets opacity and alphaTest to 0 to make it fully invisible. 241 | * If temporal is disabled, all shadow frames will be rendered in one go. 242 | */ 243 | function reset() { 244 | plm.clear() 245 | shadowMaterial.opacity = 0 246 | shadowMaterial.alphaTest = 0 247 | api.count = 0 248 | 249 | // If temporal is disabled and a finite number of frames is specified, 250 | // accumulate all frames in one shot. Expect the renderer to freeze while it's computing. 251 | if (!shadowParams.temporal && shadowParams.frames !== Infinity) { 252 | renderShadows(shadowParams.frames) 253 | } 254 | } 255 | 256 | /** 257 | * Call this function in the requestAnimationFrame loop. 258 | * If temporal is true, for each frame rendered, one shadow frame is rendered, thus distributing the stress of rendering the shadows across multiple frames. 259 | */ 260 | function temporalUpdate() { 261 | // If temporal is true, accumulate one frame at a time, as long as the count is within the specified limit 262 | if ( 263 | (shadowParams.temporal || shadowParams.frames === Infinity) && 264 | api.count < shadowParams.frames && 265 | api.count < shadowParams.limit 266 | ) { 267 | renderShadows() 268 | api.count++ 269 | } 270 | } 271 | 272 | /** 273 | * Render shadows into the render target. 274 | * @param frames The number of frames to render for each call 275 | */ 276 | function renderShadows(frames = 1) { 277 | shadowParams.blend = Math.max(2, shadowParams.frames === Infinity ? shadowParams.blend : shadowParams.frames) 278 | 279 | // Adapt the opacity-blend ratio to the number of frames 280 | if (!shadowParams.temporal) { 281 | shadowMaterial.opacity = shadowParams.opacity 282 | shadowMaterial.alphaTest = shadowParams.alphaTest 283 | } else { 284 | shadowMaterial.opacity = Math.min( 285 | shadowParams.opacity, 286 | shadowMaterial.opacity + shadowParams.opacity / shadowParams.blend 287 | ) 288 | shadowMaterial.alphaTest = Math.min( 289 | shadowParams.alphaTest, 290 | shadowMaterial.alphaTest + shadowParams.alphaTest / shadowParams.blend 291 | ) 292 | } 293 | 294 | // Switch accumulative lights on 295 | scene.add(gLights) 296 | // Collect scene lights and meshes 297 | plm.prepare() 298 | // Update the lightmap and the accumulative lights 299 | for (let i = 0; i < frames; i++) { 300 | randomiseLightPositions() 301 | plm.update(camera, shadowParams.blend) 302 | } 303 | // Switch lights off 304 | scene.remove(gLights) 305 | // Restore lights and meshes 306 | plm.finish() 307 | } 308 | 309 | /** 310 | * Add Gui folders 311 | * @param gui 312 | */ 313 | function addPlmGui(gui: GUI) { 314 | const shFolder = gui.addFolder('Shadow Material') 315 | shFolder.open() 316 | shFolder.add(shadowParams, 'opacity', 0, 1).onChange((v) => { 317 | shadowMaterial.opacity = v 318 | }) 319 | shFolder.add(shadowParams, 'alphaTest', 0, 1).onChange((v) => { 320 | shadowMaterial.alphaTest = v 321 | }) 322 | shFolder.addColor(shadowMaterial, 'color') 323 | shFolder.add(shadowMaterial, 'blend', 0, 3) 324 | 325 | const folder = gui.addFolder('Shadow params') 326 | folder.open() 327 | folder.add(shadowParams, 'temporal') 328 | folder.add(api, 'count').listen().disable() 329 | 330 | const tempObject = { 331 | reComputeShadows: () => {}, // to make a button in gui 332 | } 333 | folder.add(tempObject, 'reComputeShadows').name('Re compute ⚡').onChange(reset) 334 | 335 | folder.add(shadowParams, 'frames', 2, 100, 1).onFinishChange(reset) 336 | folder 337 | .add(shadowParams, 'scale', 0.5, 30) 338 | .onChange((v: number) => { 339 | gPlane.scale.setScalar(v) 340 | }) 341 | .onFinishChange(reset) 342 | 343 | folder.add(lightParams, 'radius', 0.1, 5).onFinishChange(reset) 344 | folder.add(lightParams, 'ambient', 0, 1).onFinishChange(reset) 345 | 346 | const bulbFolder = gui.addFolder('💡 Light source') 347 | bulbFolder.open() 348 | bulbFolder.add(lightParams.position, 'x', -5, 5).name('Light Direction X').onFinishChange(reset) 349 | bulbFolder.add(lightParams.position, 'y', 1, 5).name('Light Direction Y').onFinishChange(reset) 350 | bulbFolder.add(lightParams.position, 'z', -5, 5).name('Light Direction Z').onFinishChange(reset) 351 | } 352 | 353 | AccShadowStory.storyName = 'Default' 354 | -------------------------------------------------------------------------------- /.storybook/stories/Billboard.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import { Meta } from '@storybook/html' 4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 5 | import { GUI } from 'lil-gui' 6 | import { Billboard, BillboardProps, BillboardType } from '../../src/core/Billboard' 7 | 8 | export default { 9 | title: 'Abstractions/Billboard', 10 | } as Meta 11 | 12 | let gui: GUI 13 | 14 | let globalBillboards: BillboardType[] = [] 15 | 16 | const getTorusMesh = () => { 17 | const geometry = new THREE.TorusKnotGeometry(1, 0.35, 100, 32) 18 | const mat = new THREE.MeshStandardMaterial({ 19 | roughness: 0, 20 | color: 0xffffff * Math.random(), 21 | }) 22 | const torusMesh = new THREE.Mesh(geometry, mat) 23 | torusMesh.castShadow = true 24 | torusMesh.receiveShadow = true 25 | return torusMesh 26 | } 27 | 28 | const setupLight = () => { 29 | const dirLight = new THREE.DirectionalLight(0xabcdef, 12) 30 | dirLight.position.set(15, 15, 15) 31 | dirLight.castShadow = true 32 | dirLight.shadow.mapSize.width = 1024 33 | dirLight.shadow.mapSize.height = 1024 34 | const size = 5 35 | dirLight.shadow.camera.top = size 36 | dirLight.shadow.camera.bottom = -size 37 | dirLight.shadow.camera.left = -size 38 | dirLight.shadow.camera.right = size 39 | return dirLight 40 | } 41 | 42 | export const BillboardStory = async () => { 43 | gui = new GUI({ title: 'Billboard Story', closeFolders: true }) 44 | const { renderer, scene, camera, render } = Setup() 45 | renderer.shadowMap.enabled = true 46 | camera.position.set(5, 5, 5) 47 | const controls = new OrbitControls(camera, renderer.domElement) 48 | controls.target.set(0, 1, 0) 49 | controls.update() 50 | scene.add(new THREE.AmbientLight(0xffffff, 0.1)) 51 | scene.add(new THREE.GridHelper(30)) 52 | 53 | camera.position.set(10, 10, 10) 54 | scene.add(setupLight()) 55 | 56 | const torusNormal = getTorusMesh() 57 | torusNormal.position.set(0, 8, 0) 58 | scene.add(torusNormal) 59 | 60 | // Torus billboard 61 | const torusBillboardParams = { 62 | follow: true, 63 | lockX: false, 64 | lockY: false, 65 | lockZ: false, 66 | } as BillboardProps 67 | 68 | const torusBillboard = Billboard() 69 | const torus = getTorusMesh() 70 | torus.position.set(1, 2, 0) 71 | torusBillboard.group.add(torus) 72 | scene.add(torusBillboard.group) 73 | globalBillboards.push(torusBillboard) 74 | addBillboardGui('torus', torusBillboard, torusBillboardParams) 75 | 76 | // Plane billboard 77 | const planeBillboardParams = { 78 | follow: true, 79 | lockX: false, 80 | lockY: false, 81 | lockZ: false, 82 | } as BillboardProps 83 | 84 | const texture = new THREE.TextureLoader().load('photo-1678043639454-dbdf877f0ae8.jpeg') 85 | const planeBillboard = Billboard() 86 | const plane = new THREE.Mesh(new THREE.PlaneGeometry(3, 3), new THREE.MeshStandardMaterial({ map: texture })) 87 | plane.position.set(-3, 2, -5) 88 | planeBillboard.group.add(plane) 89 | scene.add(planeBillboard.group) 90 | globalBillboards.push(planeBillboard) 91 | addBillboardGui('plane', planeBillboard, planeBillboardParams) 92 | 93 | render(() => { 94 | globalBillboards.forEach((billboard) => { 95 | billboard.update(camera) 96 | }) 97 | }) 98 | } 99 | 100 | const addBillboardGui = (name: string, billboard: BillboardType, params: BillboardProps) => { 101 | const folder = gui.addFolder(name + ' Billboard') 102 | folder.open() 103 | 104 | folder.add(params, 'follow').onChange((value: boolean) => { 105 | billboard.updateProps({ follow: value }) 106 | }) 107 | folder.add(params, 'lockX').onChange((value: boolean) => { 108 | billboard.updateProps({ lockX: value }) 109 | }) 110 | folder.add(params, 'lockY').onChange((value: boolean) => { 111 | billboard.updateProps({ lockY: value }) 112 | }) 113 | folder.add(params, 'lockZ').onChange((value: boolean) => { 114 | billboard.updateProps({ lockZ: value }) 115 | }) 116 | } 117 | 118 | BillboardStory.storyName = 'Default' 119 | -------------------------------------------------------------------------------- /.storybook/stories/Caustics.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import { Meta } from '@storybook/html' 4 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js' 5 | import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js' 6 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 7 | 8 | import { GUI } from 'lil-gui' 9 | import { Caustics, CausticsType } from '../../src/core/Caustics' 10 | 11 | export default { 12 | title: 'Shaders/Caustics', 13 | } as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS 14 | let gui: GUI 15 | let torusMesh: THREE.Mesh 16 | let caustics: CausticsType 17 | 18 | const params = { 19 | animate: true, 20 | } 21 | 22 | export const CausticsStory = async () => { 23 | gui = new GUI({ title: 'Caustics Story', closeFolders: true }) 24 | const { renderer, scene, camera, render } = Setup() 25 | renderer.shadowMap.enabled = true 26 | camera.position.set(-15, 15, 15) 27 | const controls = new OrbitControls(camera, renderer.domElement) 28 | controls.target.set(0, 0.5, 0) 29 | controls.update() 30 | 31 | const floor = new THREE.Mesh( 32 | new THREE.PlaneGeometry(60, 60).rotateX(-Math.PI / 2), 33 | new THREE.ShadowMaterial({ opacity: 0.3 }) 34 | ) 35 | floor.receiveShadow = true 36 | scene.add(floor) 37 | 38 | const dirLight = new THREE.DirectionalLight(0xabcdef, 5) 39 | dirLight.position.set(15, 15, 15) 40 | dirLight.castShadow = true 41 | dirLight.shadow.mapSize.width = 1024 42 | dirLight.shadow.mapSize.height = 1024 43 | dirLight.shadow.camera.top = 15 44 | dirLight.shadow.camera.bottom = -15 45 | dirLight.shadow.camera.left = -15 46 | dirLight.shadow.camera.right = 15 47 | scene.add(dirLight) 48 | 49 | const folder = gui.addFolder('Light Settings') 50 | folder.add(dirLight, 'intensity', 0, 5) 51 | folder.addColor(dirLight, 'color') 52 | folder.add(dirLight.position, 'x', -15, 15).name('position x') 53 | folder.add(dirLight.position, 'y', -15, 15).name('position y') 54 | folder.add(dirLight.position, 'z', -15, 15).name('position z') 55 | 56 | setupEnvironment(scene) 57 | setupCaustics(scene, renderer) 58 | 59 | render((time) => { 60 | controls.update() 61 | if (params.animate) { 62 | torusMesh.rotation.x = time / 5000 63 | torusMesh.rotation.y = time / 2500 64 | } 65 | caustics.update() // render caustics 66 | }) 67 | } 68 | 69 | /** 70 | * Add scene.environment and groundProjected skybox 71 | */ 72 | const setupEnvironment = (scene: THREE.Scene) => { 73 | const exrLoader = new EXRLoader() 74 | 75 | // exr from polyhaven.com 76 | exrLoader.load('round_platform_1k.exr', (exrTex) => { 77 | exrTex.mapping = THREE.EquirectangularReflectionMapping 78 | scene.environment = exrTex 79 | scene.background = exrTex 80 | 81 | const groundProjection = new GroundedSkybox(exrTex, 5, 50) 82 | groundProjection.position.set(0, 5, 0) 83 | scene.add(groundProjection) 84 | }) 85 | } 86 | 87 | const setupCaustics = (scene: THREE.Scene, renderer: THREE.WebGLRenderer) => { 88 | const geometry = new THREE.TorusKnotGeometry(3, 1, 100, 32) 89 | const mat = new THREE.MeshPhysicalMaterial({ 90 | transmission: 1, 91 | roughness: 0, 92 | }) 93 | mat.color.setHSL(Math.random(), 1, 0.5) 94 | mat.thickness = 2 95 | torusMesh = new THREE.Mesh(geometry, mat) 96 | torusMesh.material 97 | torusMesh.position.set(0, 6, 0) 98 | 99 | torusMesh.traverse((child) => { 100 | if (child instanceof THREE.Mesh) { 101 | child.castShadow = true 102 | child.receiveShadow = true 103 | } 104 | }) 105 | 106 | caustics = Caustics(renderer, { frames: Infinity, resolution: 1024, worldRadius: 0.3 }) 107 | caustics.helper.visible = false // start hidden 108 | scene.add(caustics.group, caustics.helper) 109 | 110 | caustics.group.position.y = 0.001 // to prevent z-fighting with GroundedSkybox 111 | 112 | caustics.scene.add(torusMesh) 113 | 114 | addCausticsGui(caustics) 115 | 116 | const torusGui = gui.addFolder('Torus') 117 | torusGui.addColor(mat, 'color') 118 | torusGui.add(mat, 'roughness', 0, 1) 119 | torusGui.add(mat, 'transmission', 0, 1) 120 | torusGui.add(params, 'animate') 121 | } 122 | 123 | function addCausticsGui(caustics: CausticsType) { 124 | const folder = gui.addFolder('Caustics') 125 | folder.open() 126 | folder.addColor(caustics.params, 'color') 127 | folder.add(caustics.params, 'ior', 0, Math.PI) 128 | folder.add(caustics.params, 'far', 0, 15).name('Caustics Far') 129 | 130 | folder.add(caustics.helper, 'visible').name('helper') 131 | 132 | folder.add(caustics.params, 'backside').onChange((v) => { 133 | if (!v) { 134 | // to prevent last frame from persisting 135 | caustics.causticsTargetB.dispose() 136 | } 137 | }) 138 | folder.add(caustics.params, 'backsideIOR', 0, Math.PI) 139 | folder.add(caustics.params, 'worldRadius', 0.001, 0.5) 140 | folder.add(caustics.params, 'intensity', 0, 1) 141 | folder.add(caustics.params, 'causticsOnly') 142 | 143 | // params.lightSource can be vector3 or an object3d 144 | if (caustics.params.lightSource instanceof THREE.Vector3) { 145 | folder.add(caustics.params.lightSource, 'x', -1, 1).name('lightSource X') 146 | folder.add(caustics.params.lightSource, 'y', -1, 1).name('lightSource Y') 147 | folder.add(caustics.params.lightSource, 'z', -1, 1).name('lightSource Z') 148 | } 149 | } 150 | 151 | CausticsStory.storyName = 'Default' 152 | -------------------------------------------------------------------------------- /.storybook/stories/Clouds.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import GUI from 'lil-gui' 4 | import { Meta } from '@storybook/html' 5 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 6 | import { CLOUD_URL, Clouds, Cloud } from '../../src/core/Cloud' 7 | 8 | export default { 9 | title: 'Staging/Clouds', 10 | } as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS 11 | 12 | let gui: GUI 13 | let scene: THREE.Scene, 14 | camera: THREE.PerspectiveCamera, 15 | renderer: THREE.WebGLRenderer, 16 | clock: THREE.Clock, 17 | animateLoop: (arg0: (time: number) => void) => void 18 | 19 | const textureLoader = new THREE.TextureLoader() 20 | 21 | export const CloudStory = async () => { 22 | const setupResult = Setup() 23 | scene = setupResult.scene 24 | camera = setupResult.camera 25 | renderer = setupResult.renderer 26 | animateLoop = setupResult.render 27 | clock = new THREE.Clock() 28 | 29 | gui = new GUI({ title: CloudStory.storyName }) 30 | renderer.shadowMap.enabled = true 31 | renderer.toneMapping = THREE.ACESFilmicToneMapping 32 | camera.position.set(12, 3, 12) 33 | 34 | new OrbitControls(camera, renderer.domElement) 35 | 36 | scene.background = new THREE.Color('skyblue') 37 | setupLights() 38 | setupCloud() 39 | } 40 | 41 | const setupLights = () => { 42 | // cloud's default material does not react to hdri, so we need to add punctual lights 43 | 44 | const lightFol = gui.addFolder('Lights').close() 45 | const ambientLight = new THREE.AmbientLight() 46 | scene.add(ambientLight) 47 | lightFol.add(ambientLight, 'intensity', 0, 3) 48 | const guiParams = { 49 | lightHelpers: false, 50 | } 51 | 52 | const lightHelpers: THREE.SpotLightHelper[] = [] 53 | 54 | lightFol.add(guiParams, 'lightHelpers').onChange((v: boolean) => { 55 | lightHelpers.forEach((helper) => (helper.visible = v)) 56 | }) 57 | 58 | function addSpotlightGui(spotLight: THREE.SpotLight) { 59 | const fol = lightFol.addFolder('spotlight') 60 | fol.onChange(() => { 61 | lightHelpers.forEach((helper) => helper.update()) 62 | }) 63 | 64 | fol.addColor(spotLight, 'color') 65 | fol.add(spotLight, 'intensity', 0, 30) 66 | fol.add(spotLight, 'angle', 0, Math.PI / 8) 67 | 68 | fol.add(spotLight, 'penumbra', -1, 1) 69 | } 70 | 71 | const spotLight1 = new THREE.SpotLight() 72 | spotLight1.intensity = 30 73 | spotLight1.position.fromArray([0, 40, 0]) 74 | spotLight1.distance = 45 75 | spotLight1.decay = 0 76 | spotLight1.penumbra = 1 77 | spotLight1.intensity = 30 78 | const helper1 = new THREE.SpotLightHelper(spotLight1) 79 | addSpotlightGui(spotLight1) 80 | helper1.visible = false 81 | lightHelpers.push(helper1) 82 | scene.add(spotLight1, helper1) 83 | 84 | const spotLight2 = new THREE.SpotLight('red') 85 | spotLight2.intensity = 30 86 | spotLight2.position.fromArray([-20, 0, 10]) 87 | spotLight2.angle = 0.15 88 | spotLight2.decay = 0 89 | spotLight2.penumbra = -1 90 | spotLight2.intensity = 30 91 | addSpotlightGui(spotLight2) 92 | 93 | const helper2 = new THREE.SpotLightHelper(spotLight2) 94 | helper2.visible = false 95 | lightHelpers.push(helper2) 96 | scene.add(spotLight2, helper2) 97 | 98 | const spotLight3 = new THREE.SpotLight('green') 99 | spotLight3.intensity = 30 100 | spotLight3.position.fromArray([20, -10, 10]) 101 | spotLight3.angle = 0.2 102 | spotLight3.decay = 0 103 | spotLight3.penumbra = -1 104 | spotLight3.intensity = 20 105 | addSpotlightGui(spotLight3) 106 | 107 | const helper3 = new THREE.SpotLightHelper(spotLight3) 108 | helper3.visible = false 109 | lightHelpers.push(helper3) 110 | scene.add(spotLight3, helper3) 111 | } 112 | 113 | const setupCloud = async () => { 114 | const cloudTexture = await textureLoader.loadAsync(CLOUD_URL) 115 | 116 | // create main clouds group 117 | const clouds = new Clouds({ texture: cloudTexture }) 118 | scene.add(clouds) 119 | 120 | // create first cloud 121 | const cloud0 = new Cloud() 122 | clouds.add(cloud0) 123 | addCloudGui(cloud0) 124 | 125 | // create second cloud 126 | const cloud1 = new Cloud() 127 | cloud1.color.set('#111111') 128 | cloud1.position.set(-10, 4, -5) 129 | clouds.add(cloud1) 130 | addCloudGui(cloud1) 131 | 132 | animateLoop(() => { 133 | // update clouds on each frame 134 | clouds.update(camera, clock.getElapsedTime(), clock.getDelta()) 135 | }) 136 | } 137 | 138 | const addCloudGui = (cloud: Cloud) => { 139 | const fol = gui.addFolder('Edit: ' + cloud.name) 140 | 141 | // during runtime call "cloud.updateCloud()" after changing any cloud property 142 | fol.onChange(() => cloud.updateCloud()) 143 | 144 | fol.addColor(cloud, 'color') 145 | fol.add(cloud, 'seed', 0, 100, 1) 146 | fol.add(cloud, 'segments', 1, 80, 1) 147 | fol.add(cloud, 'volume', 0, 100, 0.1) 148 | fol.add(cloud, 'opacity', 0, 1, 0.01) 149 | fol.add(cloud, 'fade', 0, 400, 1) 150 | fol.add(cloud, 'growth', 0, 20, 1) 151 | fol.add(cloud, 'speed', 0, 1, 0.01) 152 | 153 | const bFol = fol.addFolder('bounds').close() 154 | bFol.add(cloud.bounds, 'x', 0, 25, 0.5) 155 | bFol.add(cloud.bounds, 'y', 0, 25, 0.5) 156 | bFol.add(cloud.bounds, 'z', 0, 25, 0.5) 157 | 158 | const pFol = fol.addFolder('position').close() 159 | pFol.add(cloud.position, 'x', -10, 10, 0.1) 160 | pFol.add(cloud.position, 'y', -10, 10, 0.1) 161 | pFol.add(cloud.position, 'z', -10, 10, 0.1) 162 | return fol 163 | } 164 | 165 | CloudStory.storyName = 'Two Clouds' 166 | -------------------------------------------------------------------------------- /.storybook/stories/Grid.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import { Meta } from '@storybook/html' 4 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js' 5 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 6 | 7 | import { GUI } from 'lil-gui' 8 | import { Grid, GridType } from '../../src/core/Grid' 9 | 10 | export default { 11 | title: 'Gizmos/Grid', 12 | } as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS 13 | let gui: GUI 14 | 15 | let grid: GridType 16 | 17 | export const GridStory = async () => { 18 | gui = new GUI({ title: 'Grid Story', closeFolders: true }) 19 | const { renderer, scene, camera, render } = Setup() 20 | renderer.shadowMap.enabled = true 21 | camera.position.set(5, 5, 5) 22 | const controls = new OrbitControls(camera, renderer.domElement) 23 | controls.target.set(0, 1, 0) 24 | controls.update() 25 | 26 | const floor = new THREE.Mesh( 27 | new THREE.PlaneGeometry(60, 60).rotateX(-Math.PI / 2), 28 | new THREE.ShadowMaterial({ opacity: 0.3, transparent: true, depthWrite: false, depthTest: true }) 29 | ) 30 | floor.receiveShadow = true 31 | scene.add(floor) 32 | 33 | const dirLight = new THREE.DirectionalLight(0xabcdef, 5) 34 | dirLight.position.set(15, 15, 15) 35 | dirLight.castShadow = true 36 | dirLight.shadow.mapSize.width = 1024 37 | dirLight.shadow.mapSize.height = 1024 38 | const size = 5 39 | dirLight.shadow.camera.top = size 40 | dirLight.shadow.camera.bottom = -size 41 | dirLight.shadow.camera.left = -size 42 | dirLight.shadow.camera.right = size 43 | scene.add(dirLight) 44 | 45 | const geometry = new THREE.TorusKnotGeometry(1, 0.35, 100, 32) 46 | const mat = new THREE.MeshStandardMaterial({ 47 | roughness: 0, 48 | }) 49 | mat.color.setHSL(Math.random(), 1, 0.5) 50 | const torusMesh = new THREE.Mesh(geometry, mat) 51 | torusMesh.position.set(0, 2, 0) 52 | 53 | torusMesh.traverse((child) => { 54 | if (child instanceof THREE.Mesh) { 55 | child.castShadow = true 56 | child.receiveShadow = true 57 | } 58 | }) 59 | scene.add(torusMesh) 60 | 61 | const folder = gui.addFolder('Light Settings') 62 | folder.add(dirLight, 'intensity', 0, 5) 63 | folder.addColor(dirLight, 'color') 64 | folder.add(dirLight.position, 'x', -15, 15).name('position x') 65 | folder.add(dirLight.position, 'y', -15, 15).name('position y') 66 | folder.add(dirLight.position, 'z', -15, 15).name('position z') 67 | 68 | setupEnvironment(scene) 69 | setupGrid(scene) 70 | 71 | render((time) => { 72 | controls.update() 73 | grid.update(camera) 74 | }) 75 | } 76 | 77 | /** 78 | * Add scene.environment and groundProjected skybox 79 | */ 80 | const setupEnvironment = (scene: THREE.Scene) => { 81 | const exrLoader = new EXRLoader() 82 | 83 | // exr from polyhaven.com 84 | exrLoader.load('round_platform_1k.exr', (exrTex) => { 85 | exrTex.mapping = THREE.EquirectangularReflectionMapping 86 | scene.environment = exrTex 87 | scene.background = exrTex 88 | 89 | scene.backgroundBlurriness = 0.7 90 | scene.backgroundIntensity = 0.1 91 | }) 92 | } 93 | 94 | function setupGrid(scene: THREE.Scene) { 95 | grid = Grid({ 96 | args: [10.5, 10.5], 97 | cellSize: 0.6, 98 | cellThickness: 1, 99 | cellColor: new THREE.Color('#6f6f6f'), 100 | sectionSize: 3.3, 101 | sectionThickness: 1.5, 102 | sectionColor: new THREE.Color('#9d4b4b'), 103 | fadeDistance: 25, 104 | fadeStrength: 1, 105 | followCamera: false, 106 | infiniteGrid: true, 107 | }) 108 | grid.mesh.position.y = 0.005 // to prevent z-fighting with existing meshes 109 | 110 | scene.add(grid.mesh) 111 | 112 | // don't forget to add " grid.update(camera) " in your animate loop 113 | 114 | addGridGui(grid) 115 | } 116 | 117 | const addGridGui = (grid: GridType) => { 118 | const folder = gui.addFolder('G R I D') 119 | folder.open() 120 | folder.addColor(grid.mesh.material, 'cellColor') 121 | folder.add(grid.mesh.material, 'cellSize', 0.01, 2, 0.1) 122 | folder.add(grid.mesh.material, 'cellThickness', 0, 5) 123 | 124 | folder.addColor(grid.mesh.material, 'sectionColor') 125 | folder.add(grid.mesh.material, 'sectionSize', 0.01, 2, 0.1) 126 | folder.add(grid.mesh.material, 'sectionThickness', 0, 5) 127 | 128 | folder.add(grid.mesh.material, 'fadeDistance', 0, 50) 129 | folder.add(grid.mesh.material, 'fadeStrength', 0, 1) 130 | folder.add(grid.mesh.material, 'followCamera') 131 | folder.add(grid.mesh.material, 'infiniteGrid') 132 | const sideOptions = { 133 | FrontSide: THREE.FrontSide, 134 | BackSide: THREE.BackSide, 135 | DoubleSide: THREE.DoubleSide, 136 | } 137 | folder.add(grid.mesh.material, 'side', sideOptions) 138 | 139 | const tFol = folder.addFolder('Transforms') 140 | tFol.add(grid.mesh.position, 'x', -3, 3, 0.1).name('Position x') 141 | tFol.add(grid.mesh.position, 'y', -3, 3, 0.1).name('Position y') 142 | tFol.add(grid.mesh.position, 'z', -3, 3, 0.1).name('Position z') 143 | 144 | tFol.add(grid.mesh.rotation, 'x', 0, 2 * Math.PI, 0.1).name('Rotation x') 145 | tFol.add(grid.mesh.rotation, 'y', 0, 2 * Math.PI, 0.1).name('Rotation y') 146 | tFol.add(grid.mesh.rotation, 'z', 0, 2 * Math.PI, 0.1).name('Rotation z') 147 | } 148 | 149 | GridStory.storyName = 'Default' 150 | -------------------------------------------------------------------------------- /.storybook/stories/MeshDistortMaterial.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import GUI from 'lil-gui' 4 | import { Meta } from '@storybook/html' 5 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 6 | import { MeshDistortMaterial } from '../../src/materials/MeshDistortMaterial' 7 | 8 | export default { 9 | title: 'Shaders/MeshDistortMaterial', 10 | } as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS 11 | 12 | let gui: GUI, materialGuiFolder: GUI 13 | let scene: THREE.Scene, 14 | camera: THREE.PerspectiveCamera, 15 | renderer: THREE.WebGLRenderer, 16 | animateLoop: (arg0: (time: number) => void) => void, 17 | meshDistortMaterial: MeshDistortMaterial 18 | 19 | export const MDMStory = async () => { 20 | const setupResult = Setup() 21 | scene = setupResult.scene 22 | camera = setupResult.camera 23 | renderer = setupResult.renderer 24 | animateLoop = setupResult.render 25 | 26 | gui = new GUI({ title: MDMStory.storyName }) 27 | camera.position.set(0, 0, 5) 28 | 29 | const controls = new OrbitControls(camera, renderer.domElement) 30 | controls.update() 31 | 32 | const ambientLight = new THREE.AmbientLight() 33 | scene.add(ambientLight) 34 | 35 | const dirLight = new THREE.DirectionalLight(0xabcdef, 10) 36 | dirLight.position.set(1, 20, 1) 37 | dirLight.castShadow = true 38 | dirLight.shadow.mapSize.width = 1024 39 | dirLight.shadow.mapSize.height = 1024 40 | scene.add(dirLight) 41 | 42 | setupMeshDistortMaterial() 43 | } 44 | 45 | async function setupMeshDistortMaterial() { 46 | const geometry = new THREE.SphereGeometry(1, 32, 32) 47 | meshDistortMaterial = new MeshDistortMaterial({ color: '#f25042', radius: 0.2 }) 48 | 49 | const mesh = new THREE.Mesh(geometry, meshDistortMaterial) 50 | mesh.scale.set(2, 4, 1) 51 | 52 | scene.add(mesh) 53 | 54 | createMeshDistortGUI() 55 | 56 | animateLoop((time) => { 57 | meshDistortMaterial.time = time * 0.0025 58 | meshDistortMaterial.distort = THREE.MathUtils.lerp(meshDistortMaterial.distort, 0.4, 0.05) 59 | }) 60 | } 61 | 62 | /** 63 | * Create gui for material properties 64 | */ 65 | function createMeshDistortGUI() { 66 | if (materialGuiFolder) { 67 | materialGuiFolder.destroy() 68 | } 69 | 70 | const matProps = gui.addFolder('MeshDistortMaterial id: ' + meshDistortMaterial.id) 71 | 72 | matProps.addColor(meshDistortMaterial, 'color') 73 | matProps.add(meshDistortMaterial._radius, 'value').min(0.01).max(0.5).step(0.01).name('radius') 74 | 75 | materialGuiFolder = matProps 76 | } 77 | 78 | MDMStory.storyName = 'Sphere' 79 | -------------------------------------------------------------------------------- /.storybook/stories/MeshPortalMaterial.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import GUI from 'lil-gui' 3 | import { Meta } from '@storybook/html' 4 | import { Setup } from '../Setup' 5 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 6 | import { MeshPortalMaterial } from '../../src/core/MeshPortalMaterial' 7 | import { EXRLoader } from 'three/examples/jsm/Addons.js' 8 | export default { 9 | title: 'Shaders/MeshPortalMaterial/basic', 10 | } as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS 11 | 12 | let gui: GUI 13 | let scene: THREE.Scene, 14 | portalScene: THREE.Scene, 15 | camera: THREE.PerspectiveCamera, 16 | renderer: THREE.WebGLRenderer, 17 | portalMesh: THREE.Mesh 18 | 19 | const rendererSize = new THREE.Vector2() 20 | 21 | const portalParams = { 22 | resolution: 1024, 23 | renderTarget: new THREE.WebGLRenderTarget(), 24 | } 25 | 26 | export const MPMStory = async () => { 27 | gui = new GUI({ title: MPMStory.storyName }) 28 | 29 | renderer = new THREE.WebGLRenderer({ alpha: true, canvas, context }) 30 | renderer.toneMapping = THREE.ACESFilmicToneMapping 31 | 32 | scene = new THREE.Scene() 33 | portalScene = new THREE.Scene() 34 | 35 | camera = new THREE.PerspectiveCamera(45, 1, 1, 1000) 36 | camera.position.set(2.5, 0, 2.5) 37 | 38 | const controls = new OrbitControls(camera, renderer.domElement) 39 | controls.update() 40 | 41 | setupMainScene() 42 | setupPortalScene() 43 | 44 | const onResize = () => { 45 | // resize canvas 46 | renderer.setPixelRatio(Math.min(2, Math.max(1, window.devicePixelRatio))) 47 | renderer.setSize(root.clientWidth, root.clientHeight) 48 | camera.aspect = root.clientWidth / root.clientHeight 49 | camera.updateProjectionMatrix() 50 | 51 | // update 'rendererSize' vector 52 | renderer.getSize(rendererSize) 53 | rendererSize.multiplyScalar(renderer.getPixelRatio()) 54 | } 55 | 56 | onResize() 57 | window.addEventListener('resize', onResize) 58 | 59 | renderer.setAnimationLoop(() => { 60 | // render portal scene 61 | renderer.setRenderTarget(portalParams.renderTarget) 62 | renderer.render(portalScene, camera) 63 | renderer.setRenderTarget(null) 64 | 65 | // render main scene 66 | renderer.render(scene, camera) 67 | }) 68 | } 69 | 70 | function setupMainScene() { 71 | // in the main scene use basic lights 72 | const ambientLight = new THREE.AmbientLight() 73 | scene.add(ambientLight) 74 | 75 | const dirLight = new THREE.DirectionalLight(0xabcdef, 10) 76 | dirLight.position.set(1, 20, 1) 77 | scene.add(dirLight) 78 | 79 | scene.background = new THREE.Color().set(0xffffff * Math.random()) 80 | const geometry = new THREE.TorusKnotGeometry(0.5, 0.25, 150, 20) 81 | const material = new THREE.MeshStandardMaterial({ 82 | metalness: 0, 83 | roughness: 0.2, 84 | color: 0xffffff * Math.random(), 85 | }) 86 | const torusMesh = new THREE.Mesh(geometry, material) 87 | portalScene.add(torusMesh) 88 | torusMesh.position.z = -1 89 | scene.add(torusMesh) 90 | 91 | // add GUI 92 | const fol = gui.addFolder('Main scene') 93 | fol.open() 94 | fol.addColor(scene, 'background') 95 | fol.addColor(material, 'color').name('torus color') 96 | } 97 | 98 | function setupPortalScene() { 99 | // in the portal scene just use and hdri for lighting 100 | const exrLoader = new EXRLoader() 101 | exrLoader.load('round_platform_1k.exr', (exrTex) => { 102 | // exr from polyhaven.com 103 | exrTex.mapping = THREE.EquirectangularReflectionMapping 104 | portalScene.environment = exrTex 105 | portalScene.background = exrTex 106 | }) 107 | 108 | // setup the portal 109 | portalParams.renderTarget.setSize(portalParams.resolution, portalParams.resolution) 110 | 111 | renderer.getSize(rendererSize) 112 | rendererSize.multiplyScalar(renderer.getPixelRatio()) 113 | 114 | const portalGeometry = new THREE.PlaneGeometry(2, 2) 115 | const portalMaterial = new MeshPortalMaterial({ 116 | map: portalParams.renderTarget.texture, 117 | resolution: rendererSize, 118 | }) 119 | 120 | portalMesh = new THREE.Mesh(portalGeometry, portalMaterial) 121 | scene.add(portalMesh) 122 | 123 | const geometry = new THREE.TorusKnotGeometry(0.5, 0.25, 150, 20) 124 | const material = new THREE.MeshStandardMaterial({ 125 | metalness: 1, 126 | roughness: 0.2, 127 | color: 0xffffff * Math.random(), 128 | }) 129 | const torusMesh = new THREE.Mesh(geometry, material) 130 | torusMesh.position.z = -1 131 | portalScene.add(torusMesh) 132 | 133 | // add gui 134 | const fol = gui.addFolder('Portal Scene') 135 | fol.open() 136 | fol.add(portalScene, 'backgroundBlurriness', 0, 1) 137 | fol.addColor(material, 'color').name('torus color') 138 | 139 | const pFol = fol.addFolder('Portal settings') 140 | pFol.add(portalParams, 'resolution', 128, 2048, 256).onChange(() => { 141 | portalParams.renderTarget.setSize(portalParams.resolution, portalParams.resolution) 142 | }) 143 | pFol.add(portalMesh.material, 'toneMapped') 144 | pFol.add(portalMesh.scale, 'x', 0.1, 2).name('scale X') 145 | pFol.add(portalMesh.scale, 'y', 0.1, 2).name('scale Y') 146 | } 147 | 148 | MPMStory.storyName = 'plane' 149 | -------------------------------------------------------------------------------- /.storybook/stories/MeshPortalMaterialSdf.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import GUI from 'lil-gui' 3 | import { Meta } from '@storybook/html' 4 | import { Setup } from '../Setup' 5 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 6 | import { MeshPortalMaterial, meshPortalMaterialApplySDF } from '../../src/core/MeshPortalMaterial' 7 | import { EXRLoader } from 'three/examples/jsm/Addons.js' 8 | 9 | export default { 10 | title: 'Shaders/MeshPortalMaterial/sdf', 11 | } as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS 12 | 13 | let gui: GUI 14 | let scene: THREE.Scene, 15 | portalScene: THREE.Scene, 16 | camera: THREE.PerspectiveCamera, 17 | renderer: THREE.WebGLRenderer, 18 | portalMesh: THREE.Mesh 19 | 20 | const rendererSize = new THREE.Vector2() 21 | 22 | const portalParams = { 23 | resolution: 1024, 24 | renderTarget: new THREE.WebGLRenderTarget(), 25 | } 26 | 27 | export const MPMStory = async () => { 28 | gui = new GUI({ title: MPMStory.storyName }) 29 | 30 | renderer = new THREE.WebGLRenderer({ alpha: true, canvas, context }) 31 | renderer.toneMapping = THREE.ACESFilmicToneMapping 32 | 33 | scene = new THREE.Scene() 34 | portalScene = new THREE.Scene() 35 | 36 | camera = new THREE.PerspectiveCamera(45, 1, 1, 1000) 37 | camera.position.set(2.5, 0, 2.5) 38 | 39 | const controls = new OrbitControls(camera, renderer.domElement) 40 | controls.update() 41 | 42 | setupMainScene() 43 | setupPortalScene() 44 | 45 | const onResize = () => { 46 | // resize canvas 47 | renderer.setPixelRatio(Math.min(2, Math.max(1, window.devicePixelRatio))) 48 | renderer.setSize(root.clientWidth, root.clientHeight) 49 | camera.aspect = root.clientWidth / root.clientHeight 50 | camera.updateProjectionMatrix() 51 | 52 | // update 'rendererSize' vector 53 | renderer.getSize(rendererSize) 54 | rendererSize.multiplyScalar(renderer.getPixelRatio()) 55 | } 56 | 57 | onResize() 58 | window.addEventListener('resize', onResize) 59 | 60 | renderer.setAnimationLoop(() => { 61 | // render portal scene 62 | renderer.setRenderTarget(portalParams.renderTarget) 63 | renderer.render(portalScene, camera) 64 | renderer.setRenderTarget(null) 65 | 66 | // render main scene 67 | renderer.render(scene, camera) 68 | }) 69 | } 70 | 71 | /** 72 | * Setup the main/root scene 73 | */ 74 | function setupMainScene() { 75 | // in the main scene use basic lights 76 | const ambientLight = new THREE.AmbientLight() 77 | scene.add(ambientLight) 78 | 79 | const dirLight = new THREE.DirectionalLight(0xabcdef, 10) 80 | dirLight.position.set(1, 20, 1) 81 | scene.add(dirLight) 82 | 83 | scene.background = new THREE.Color().set(0xffffff * Math.random()) 84 | const geometry = new THREE.TorusKnotGeometry(0.5, 0.25, 150, 20) 85 | const material = new THREE.MeshStandardMaterial({ 86 | metalness: 0, 87 | roughness: 0.2, 88 | color: 0xffffff * Math.random(), 89 | }) 90 | const torusMesh = new THREE.Mesh(geometry, material) 91 | portalScene.add(torusMesh) 92 | torusMesh.position.z = -1 93 | scene.add(torusMesh) 94 | 95 | // add GUI 96 | const fol = gui.addFolder('Main scene') 97 | fol.open() 98 | fol.addColor(scene, 'background') 99 | fol.addColor(material, 'color').name('torus color') 100 | } 101 | 102 | /** 103 | * Setup the portal and the contents in the portalScene 104 | */ 105 | function setupPortalScene() { 106 | // in the portal scene just use and hdri for lighting 107 | const exrLoader = new EXRLoader() 108 | exrLoader.load('round_platform_1k.exr', (exrTex) => { 109 | // exr from polyhaven.com 110 | exrTex.mapping = THREE.EquirectangularReflectionMapping 111 | portalScene.environment = exrTex 112 | portalScene.background = exrTex 113 | }) 114 | 115 | // setup the portal 116 | portalParams.renderTarget.setSize(portalParams.resolution, portalParams.resolution) 117 | 118 | renderer.getSize(rendererSize) 119 | rendererSize.multiplyScalar(renderer.getPixelRatio()) 120 | 121 | const portalGeometry = new THREE.CircleGeometry(1.5, 64) 122 | const portalMaterial = new MeshPortalMaterial({ 123 | map: portalParams.renderTarget.texture, 124 | resolution: rendererSize, 125 | transparent: true, 126 | blur: 0.5, 127 | }) 128 | 129 | portalMesh = new THREE.Mesh(portalGeometry, portalMaterial) 130 | meshPortalMaterialApplySDF(portalMesh, 512, renderer) 131 | scene.add(portalMesh) 132 | 133 | // add another torusKnot in the same spot as the main scene 134 | const geometry = new THREE.TorusKnotGeometry(0.5, 0.25, 150, 20) 135 | const material = new THREE.MeshStandardMaterial({ 136 | metalness: 1, 137 | roughness: 0.2, 138 | color: 0xffffff * Math.random(), 139 | }) 140 | const torusMesh = new THREE.Mesh(geometry, material) 141 | torusMesh.position.z = -1 142 | portalScene.add(torusMesh) 143 | 144 | // add gui 145 | const fol = gui.addFolder('Portal Scene') 146 | fol.open() 147 | fol.add(portalScene, 'backgroundBlurriness', 0, 1) 148 | fol.addColor(material, 'color').name('torus color') 149 | 150 | const pFol = fol.addFolder('Portal settings') 151 | pFol.add(portalMesh.material, 'blur', 0, 2) 152 | pFol.add(portalParams, 'resolution', 128, 2048, 256).onChange(() => { 153 | portalParams.renderTarget.setSize(portalParams.resolution, portalParams.resolution) 154 | }) 155 | pFol.add(portalMesh.material, 'toneMapped') 156 | 157 | pFol.add(portalMesh.scale, 'x', 0.1, 2).name('scale X') 158 | pFol.add(portalMesh.scale, 'y', 0.1, 2).name('scale Y') 159 | } 160 | 161 | MPMStory.storyName = 'circle' 162 | -------------------------------------------------------------------------------- /.storybook/stories/MeshTransmissionMaterial.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import GUI from 'lil-gui' 4 | import { Meta } from '@storybook/html' 5 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js' 6 | import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js' 7 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 8 | 9 | import { MeshTransmissionMaterial } from '../../src/materials/MeshTransmissionMaterial' 10 | import { MeshDiscardMaterial } from '../../src/materials/MeshDiscardMaterial' 11 | import { useFBO } from '../../src/core/useFBO' 12 | 13 | export default { 14 | title: 'Shaders/MeshTransmissionMaterial', 15 | } as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS 16 | 17 | const mtmParams = { 18 | backside: true, 19 | thickness: 1, 20 | backsideThickness: 0.5, 21 | } 22 | 23 | let gui: GUI 24 | let scene: THREE.Scene, camera: THREE.Camera, renderer: THREE.WebGLRenderer, animateLoop 25 | export const MTMStory = async () => { 26 | const setupResult = Setup() 27 | scene = setupResult.scene 28 | camera = setupResult.camera 29 | renderer = setupResult.renderer 30 | animateLoop = setupResult.render 31 | 32 | gui = new GUI({ title: MTMStory.storyName }) 33 | renderer.shadowMap.enabled = true 34 | renderer.toneMapping = THREE.ACESFilmicToneMapping 35 | camera.position.set(12, 12, 12) 36 | 37 | const controls = new OrbitControls(camera, renderer.domElement) 38 | controls.target.set(0, 6, 0) 39 | controls.update() 40 | 41 | const floor = new THREE.Mesh( 42 | new THREE.PlaneGeometry(60, 60).rotateX(-Math.PI / 2), 43 | new THREE.ShadowMaterial({ opacity: 0.3 }) 44 | ) 45 | floor.receiveShadow = true 46 | scene.add(floor) 47 | 48 | const dirLight = new THREE.DirectionalLight(0xabcdef, 10) 49 | dirLight.position.set(1, 20, 1) 50 | dirLight.castShadow = true 51 | dirLight.shadow.mapSize.width = 1024 52 | dirLight.shadow.mapSize.height = 1024 53 | scene.add(dirLight) 54 | 55 | setupEnvironment() 56 | setupMeshTransmissionMaterial() 57 | } 58 | 59 | /** 60 | * Add scene.environment and groundProjected skybox 61 | */ 62 | const setupEnvironment = () => { 63 | const exrLoader = new EXRLoader() 64 | 65 | // exr from polyhaven.com 66 | exrLoader.load('round_platform_1k.exr', (exrTex) => { 67 | exrTex.mapping = THREE.EquirectangularReflectionMapping 68 | scene.environment = exrTex 69 | scene.background = exrTex 70 | 71 | const groundProjection = new GroundedSkybox(exrTex, 10, 50) 72 | groundProjection.position.set(0, 10, 0) 73 | scene.add(groundProjection) 74 | }) 75 | } 76 | 77 | /** 78 | * Add a torus which uses mesh transmission material 79 | */ 80 | function setupMeshTransmissionMaterial() { 81 | const discardMaterial = new MeshDiscardMaterial() 82 | const meshTransmissionMaterial = new MeshTransmissionMaterial() 83 | 84 | const geometry = new THREE.TorusKnotGeometry(3, 1, 100, 32).translate(0, 6, 0) 85 | const model = new THREE.Mesh(geometry, meshTransmissionMaterial) 86 | 87 | const transmissionMeshes: THREE.Mesh[] = [] 88 | 89 | model.traverse((child) => { 90 | if (child instanceof THREE.Mesh) { 91 | child.castShadow = true 92 | child.receiveShadow = true 93 | 94 | transmissionMeshes.push(child) 95 | } 96 | }) 97 | scene.add(model) 98 | 99 | const fboBack = useFBO(512, 512) 100 | 101 | const fboMain = useFBO(512, 512) 102 | 103 | meshTransmissionMaterial.buffer = fboMain.texture 104 | 105 | let oldBg: THREE.Color | THREE.Texture | null 106 | let oldTone: THREE.ToneMapping 107 | let oldSide: THREE.Side 108 | const state = { 109 | gl: renderer, 110 | scene, 111 | camera, 112 | } 113 | 114 | addTransmissionGui(gui, meshTransmissionMaterial) 115 | 116 | // runs on every frame 117 | animateLoop((time: number) => { 118 | meshTransmissionMaterial.time = time * 0.001 119 | 120 | for (const mesh of transmissionMeshes) { 121 | if (meshTransmissionMaterial.buffer === fboMain.texture) { 122 | // Save defaults 123 | oldTone = state.gl.toneMapping 124 | oldBg = state.scene.background 125 | oldSide = mesh.material.side 126 | 127 | // Switch off tonemapping lest it double tone maps 128 | // Save the current background and set the HDR as the new BG 129 | // Use discardMaterial, the parent will be invisible, but it's shadows will still be cast 130 | state.gl.toneMapping = THREE.NoToneMapping 131 | mesh.material = discardMaterial 132 | 133 | if (mtmParams.backside) { 134 | // Render into the backside buffer 135 | state.gl.setRenderTarget(fboBack) 136 | state.gl.render(state.scene, state.camera) 137 | // And now prepare the material for the main render using the backside buffer 138 | mesh.material = meshTransmissionMaterial 139 | mesh.material.buffer = fboBack.texture 140 | mesh.material.thickness = mtmParams.backsideThickness 141 | mesh.material.side = THREE.BackSide 142 | } 143 | 144 | // Render into the main buffer 145 | state.gl.setRenderTarget(fboMain) 146 | state.gl.render(state.scene, state.camera) 147 | 148 | mesh.material = meshTransmissionMaterial 149 | mesh.material.thickness = mtmParams.thickness 150 | mesh.material.side = oldSide 151 | mesh.material.buffer = fboMain.texture 152 | 153 | // Set old state back 154 | state.scene.background = oldBg 155 | state.gl.setRenderTarget(null) 156 | state.gl.toneMapping = oldTone 157 | } 158 | } 159 | }) 160 | } 161 | 162 | /** 163 | * Add gui 164 | * @param gui gui instance 165 | * @param mat material instance 166 | */ 167 | function addTransmissionGui(gui: GUI, mat: MeshTransmissionMaterial) { 168 | const folder = gui.addFolder('Default options') 169 | folder.addColor(mat, 'color') 170 | folder.add(mat, 'roughness', 0, 1) 171 | 172 | folder.add(mat, 'reflectivity', 0, 1) 173 | folder.addColor(mat, 'attenuationColor') 174 | folder.add(mat, 'attenuationDistance', 0, 2) 175 | 176 | const fol = gui.addFolder('Transmission Material options') 177 | fol.open() 178 | fol.add(mat, 'chromaticAberration', 0, 2) 179 | fol.add(mat, 'anisotropicBlur', 0, 10) 180 | fol.add(mat, 'distortion', 0, 10) 181 | fol.add(mat, 'temporalDistortion', 0, 1) 182 | fol.add(mtmParams, 'backside') 183 | fol.add(mtmParams, 'thickness', 0, 4) 184 | fol.add(mtmParams, 'backsideThickness', 0, 4) 185 | } 186 | 187 | MTMStory.storyName = 'TorusKnot' 188 | -------------------------------------------------------------------------------- /.storybook/stories/MeshWobbleMaterial.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import GUI from 'lil-gui' 4 | import { Meta } from '@storybook/html' 5 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 6 | import { MeshWobbleMaterial } from '../../src/materials/MeshWobbleMaterial' 7 | 8 | export default { 9 | title: 'Shaders/MeshWobbleMaterial', 10 | } as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS 11 | 12 | let gui: GUI, materialGuiFolder: GUI 13 | let scene: THREE.Scene, 14 | camera: THREE.PerspectiveCamera, 15 | renderer: THREE.WebGLRenderer, 16 | animateLoop: (arg0: (time: number) => void) => void, 17 | meshWobbleMaterial: MeshWobbleMaterial 18 | 19 | export const MWMStory = async () => { 20 | const setupResult = Setup() 21 | scene = setupResult.scene 22 | camera = setupResult.camera 23 | renderer = setupResult.renderer 24 | animateLoop = setupResult.render 25 | 26 | gui = new GUI({ title: MWMStory.storyName }) 27 | camera.position.set(0, 0, 5) 28 | 29 | const controls = new OrbitControls(camera, renderer.domElement) 30 | controls.update() 31 | 32 | const ambientLight = new THREE.AmbientLight() 33 | scene.add(ambientLight) 34 | 35 | const dirLight = new THREE.DirectionalLight(0xabcdef, 10) 36 | dirLight.position.set(1, 20, 1) 37 | dirLight.castShadow = true 38 | dirLight.shadow.mapSize.width = 1024 39 | dirLight.shadow.mapSize.height = 1024 40 | scene.add(dirLight) 41 | 42 | setupMeshWobbleMaterial() 43 | } 44 | 45 | async function setupMeshWobbleMaterial() { 46 | const geometry = new THREE.TorusGeometry(1, 0.25, 16, 100) 47 | meshWobbleMaterial = new MeshWobbleMaterial({ color: '#f25042', factor: 0.75 }) 48 | 49 | const mesh = new THREE.Mesh(geometry, meshWobbleMaterial) 50 | 51 | scene.add(mesh) 52 | 53 | createMeshWobbleGUI() 54 | 55 | animateLoop((time) => { 56 | meshWobbleMaterial.time = time * 0.0025 57 | }) 58 | } 59 | 60 | /** 61 | * Create gui for material properties 62 | */ 63 | function createMeshWobbleGUI() { 64 | if (materialGuiFolder) { 65 | materialGuiFolder.destroy() 66 | } 67 | 68 | const matProps = gui.addFolder('MeshWobbleMaterial id: ' + meshWobbleMaterial.id) 69 | 70 | matProps.addColor(meshWobbleMaterial, 'color') 71 | matProps.add(meshWobbleMaterial._factor, 'value').min(0.5).max(4).step(0.1).name('factor') 72 | 73 | materialGuiFolder = matProps 74 | } 75 | 76 | MWMStory.storyName = 'Torus' 77 | -------------------------------------------------------------------------------- /.storybook/stories/Outlines.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import { Meta } from '@storybook/html' 4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 5 | import { GUI } from 'lil-gui' 6 | import { Outlines, OutlinesType } from '../../src/core/Outlines' 7 | 8 | export default { 9 | title: 'Abstractions/Outlines', 10 | } as Meta 11 | 12 | let gui: GUI 13 | 14 | let allOutlines: OutlinesType[] = [] 15 | 16 | const outlinesParams = { 17 | color: '#ffff00' as THREE.ColorRepresentation, 18 | thickness: 0.1, 19 | screenspace: false, 20 | } 21 | 22 | const generateOutlines = (gl: THREE.WebGLRenderer) => { 23 | return Outlines({ 24 | color: new THREE.Color(outlinesParams.color), 25 | thickness: outlinesParams.thickness, 26 | screenspace: outlinesParams.screenspace, 27 | gl, 28 | }) 29 | } 30 | 31 | const setupTourMesh = (gl: THREE.WebGLRenderer) => { 32 | const geometry = new THREE.TorusKnotGeometry(1, 0.35, 100, 32) 33 | const mat = new THREE.MeshStandardMaterial({ 34 | roughness: 0, 35 | color: 0xffffff * Math.random(), 36 | }) 37 | const torusMesh = new THREE.Mesh(geometry, mat) 38 | 39 | const outlines = generateOutlines(gl) 40 | torusMesh.traverse((child) => { 41 | if (child instanceof THREE.Mesh) { 42 | child.castShadow = true 43 | child.receiveShadow = true 44 | } 45 | }) 46 | torusMesh.position.set(0, 5, 0) 47 | torusMesh.add(outlines.group) 48 | outlines.generate() 49 | allOutlines.push(outlines) 50 | return torusMesh 51 | } 52 | 53 | const setupBox = (gl: THREE.WebGLRenderer) => { 54 | const geometry = new THREE.BoxGeometry(2, 2, 2) 55 | const mat = new THREE.MeshBasicMaterial({ color: 'grey' }) 56 | const boxMesh = new THREE.Mesh(geometry, mat) 57 | boxMesh.position.y = 1.2 58 | const outlines = generateOutlines(gl) 59 | 60 | allOutlines.push(outlines) 61 | boxMesh.add(outlines.group) 62 | boxMesh.castShadow = true 63 | outlines.generate() 64 | return boxMesh 65 | } 66 | 67 | const setupLight = () => { 68 | const dirLight = new THREE.DirectionalLight(0xffffff, 3) 69 | dirLight.position.set(15, 15, 15) 70 | dirLight.castShadow = true 71 | dirLight.shadow.mapSize.width = 1024 72 | dirLight.shadow.mapSize.height = 1024 73 | const size = 6 74 | dirLight.shadow.camera.top = size 75 | dirLight.shadow.camera.bottom = -size 76 | dirLight.shadow.camera.left = -size 77 | dirLight.shadow.camera.right = size 78 | return dirLight 79 | } 80 | 81 | export const OutlinesStory = async () => { 82 | gui = new GUI({ title: 'Outlines Story', closeFolders: true }) 83 | const { renderer, scene, camera, render } = Setup() 84 | renderer.shadowMap.enabled = true 85 | camera.position.set(5, 5, 5) 86 | const controls = new OrbitControls(camera, renderer.domElement) 87 | controls.target.set(0, 1, 0) 88 | controls.update() 89 | 90 | scene.add(new THREE.AmbientLight(0xffffff, 0.1)) 91 | 92 | camera.position.set(10, 10, 10) 93 | scene.add(setupLight()) 94 | scene.add(setupTourMesh(renderer)) 95 | 96 | const box = setupBox(renderer) 97 | scene.add(box) 98 | 99 | const floor = new THREE.Mesh( 100 | new THREE.CircleGeometry(10, 32).rotateX(-Math.PI / 2), 101 | new THREE.MeshStandardMaterial({ color: 'azure' }) 102 | ) 103 | floor.receiveShadow = true 104 | scene.add(floor) 105 | 106 | render(() => { 107 | box.rotation.y += 0.02 108 | }) 109 | 110 | addOutlineGui() 111 | } 112 | 113 | OutlinesStory.storyName = 'Default' 114 | 115 | const addOutlineGui = () => { 116 | const params = Object.assign({}, outlinesParams) 117 | const folder = gui.addFolder('O U T L I N E S') 118 | folder.open() 119 | folder.addColor(params, 'color').onChange((color: THREE.ColorRepresentation) => { 120 | allOutlines.forEach((outline) => { 121 | outline.updateProps({ color: new THREE.Color(color) }) 122 | }) 123 | }) 124 | folder.add(params, 'thickness', 0, 2, 0.01).onChange((thickness: number) => { 125 | allOutlines.forEach((outline) => { 126 | outline.updateProps({ thickness }) 127 | }) 128 | }) 129 | folder.add(params, 'screenspace').onChange((screenspace: boolean) => { 130 | allOutlines.forEach((outline) => { 131 | outline.updateProps({ screenspace }) 132 | }) 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /.storybook/stories/PCSS.stories.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ACESFilmicToneMapping, 3 | AmbientLight, 4 | BoxGeometry, 5 | Camera, 6 | CameraHelper, 7 | DirectionalLight, 8 | DirectionalLightHelper, 9 | Group, 10 | MathUtils, 11 | Mesh, 12 | MeshStandardMaterial, 13 | PlaneGeometry, 14 | Renderer, 15 | Scene, 16 | SphereGeometry, 17 | } from 'three' 18 | import { pcss } from '../../src/core/pcss' 19 | import { Setup } from '../Setup' 20 | import { Meta } from '@storybook/html' 21 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 22 | import GUI from 'lil-gui' 23 | 24 | export default { 25 | title: 'Shaders/pcss', 26 | } as Meta 27 | 28 | export const PcssStory = async () => { 29 | const gui = new GUI({ title: PcssStory.storyName }) 30 | const args = { 31 | enabled: true, 32 | size: 25, 33 | focus: 0, 34 | samples: 10, 35 | intensity: 1, 36 | } 37 | 38 | const folder = gui.addFolder('Settings') 39 | folder.onChange(() => { 40 | updatePCSS(args) 41 | }) 42 | folder.add(args, 'enabled') 43 | folder.add(args, 'size', 1, 100, 1) 44 | folder.add(args, 'focus', 0, 2, 0.1) 45 | folder.add(args, 'samples', 1, 20, 1) 46 | folder.add(args, 'intensity', 0, 1, 0.1) 47 | 48 | const { renderer, scene, camera, render } = Setup() 49 | 50 | let reset: ((gl: Renderer, scene: Scene, camera: Camera) => void) | null 51 | 52 | renderer.shadowMap.enabled = true 53 | renderer.toneMapping = ACESFilmicToneMapping 54 | 55 | camera.position.set(5, 5, 5) 56 | new OrbitControls(camera, renderer.domElement) 57 | 58 | scene.add(new AmbientLight(0x666666)) 59 | 60 | const light = new DirectionalLight(0xdfebff, 5) 61 | light.position.set(2, 8, 4) 62 | 63 | light.castShadow = true 64 | light.shadow.mapSize.width = 1024 65 | light.shadow.mapSize.height = 1024 66 | light.shadow.camera.far = 20 67 | light.shadow.intensity = 1 68 | 69 | scene.add(light) 70 | 71 | scene.add(new DirectionalLightHelper(light)) 72 | scene.add(new CameraHelper(light.shadow.camera)) 73 | 74 | // sphereGroup 75 | const sphereGroup = new Group() 76 | const sphereRadius = 0.3 77 | const geometry = new SphereGeometry(sphereRadius).translate(0, sphereRadius, 0) 78 | 79 | for (let i = 0; i < 20; i++) { 80 | const material = new MeshStandardMaterial({ 81 | color: Math.random() * 0xffffff, 82 | roughness: Math.random(), 83 | }) 84 | const sphere = new Mesh(geometry, material) 85 | sphere.position.x = MathUtils.randFloatSpread(6) 86 | sphere.position.z = MathUtils.randFloatSpread(6) 87 | sphere.castShadow = true 88 | sphere.receiveShadow = true 89 | sphereGroup.add(sphere) 90 | } 91 | scene.add(sphereGroup) 92 | 93 | // ground 94 | const groundMaterial = new MeshStandardMaterial({ color: 0x404040 }) 95 | 96 | const ground = new Mesh(new PlaneGeometry(20000, 20000, 8, 8), groundMaterial) 97 | ground.rotation.x = -Math.PI / 2 98 | ground.receiveShadow = true 99 | scene.add(ground) 100 | 101 | // column 102 | for (let i = 0; i < 5; i++) { 103 | const height = MathUtils.randFloat(1, 4) 104 | const column = new Mesh(new BoxGeometry(0.2, height, 0.2).translate(0, height / 2, 0), groundMaterial) 105 | column.castShadow = true 106 | column.receiveShadow = true 107 | scene.add(column) 108 | 109 | column.position.set(MathUtils.randFloatSpread(5), 0, MathUtils.randFloatSpread(5)) 110 | } 111 | 112 | render((time) => { 113 | // make spheres go up/down 114 | for (const [index, sphere] of sphereGroup.children.entries()) { 115 | sphere.position.y = Math.sin(time / 1000 + index) + 1 116 | } 117 | }) 118 | 119 | const updatePCSS = (args: { enabled: boolean; size: number; focus: number; samples: number; intensity: number }) => { 120 | const { enabled, size, focus, samples, intensity } = args 121 | 122 | light.shadow.intensity = intensity 123 | 124 | if (reset) { 125 | reset(renderer, scene, camera) 126 | reset = null 127 | } 128 | 129 | if (enabled) { 130 | reset = pcss({ focus, size, samples }) 131 | 132 | scene.traverse((object) => { 133 | if (object instanceof Mesh) { 134 | // renderer.properties.remove(object.material) 135 | 136 | object.material.dispose() 137 | } 138 | }) 139 | } 140 | } 141 | 142 | updatePCSS(args) 143 | } 144 | 145 | PcssStory.storyName = 'PCSS' 146 | -------------------------------------------------------------------------------- /.storybook/stories/Splat.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import { Meta } from '@storybook/html' 4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 5 | import { GUI } from 'lil-gui' 6 | import { Splat, SplatLoader } from '../../src/core/Splat' 7 | 8 | export default { 9 | title: 'Abstractions/Splat', 10 | } as Meta 11 | 12 | let gui: GUI 13 | 14 | export const SplatStory = async () => { 15 | gui = new GUI({ title: 'Splat Story', closeFolders: true }) 16 | const { renderer, scene, camera } = Setup() 17 | 18 | const controls = new OrbitControls(camera, renderer.domElement) 19 | controls.target.set(0, 1, 0) 20 | camera.position.set(10, 10, 10) 21 | controls.update() 22 | 23 | scene.background = new THREE.Color('white') 24 | 25 | loadSplats(renderer, camera, scene) 26 | } 27 | 28 | async function loadSplats(renderer: THREE.WebGLRenderer, camera: THREE.PerspectiveCamera, scene: THREE.Scene) { 29 | const cakewalk = 'https://huggingface.co/cakewalk/splat-data/resolve/main' 30 | const dylanebert = 'https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/kitchen' 31 | 32 | const loader = new SplatLoader(renderer) 33 | const [shoeSplat, plushSplat, kitchenSplat] = await Promise.all([ 34 | loader.loadAsync(`${cakewalk}/nike.splat`), 35 | loader.loadAsync(`${cakewalk}/plush.splat`), 36 | loader.loadAsync(`${dylanebert}/kitchen-7k.splat`), 37 | ]) 38 | 39 | const shoe1 = new Splat(shoeSplat, camera, { alphaTest: 0.1 }) 40 | shoe1.scale.setScalar(0.5) 41 | shoe1.position.set(0, 1.6, 2) 42 | scene.add(shoe1) 43 | 44 | // This will re-use the same data, only one load, one parse, one worker, one buffer 45 | const shoe2 = new Splat(shoeSplat, camera, { alphaTest: 0.1 }) 46 | shoe2.scale.setScalar(0.5) 47 | shoe2.position.set(0, 1.6, -1.5) 48 | shoe2.rotation.set(Math.PI, 0, Math.PI) 49 | scene.add(shoe2) 50 | 51 | const plush = new Splat(plushSplat, camera, { alphaTest: 0.1 }) 52 | plush.scale.setScalar(0.5) 53 | plush.position.set(-1.5, 1.6, 1) 54 | scene.add(plush) 55 | 56 | const kitchen = new Splat(kitchenSplat, camera) 57 | kitchen.position.set(0, 0.25, 0) 58 | scene.add(kitchen) 59 | 60 | // add gui 61 | const folder = gui.addFolder('SPLAT') 62 | 63 | folder.add(shoe1, 'visible').name('Shoe 1 visible') 64 | 65 | folder.add(shoe2, 'visible').name('Shoe 2 visible') 66 | 67 | folder.add(plush, 'visible').name('Plush visible') 68 | 69 | folder.add(kitchen, 'visible').name('Kitchen visible') 70 | } 71 | 72 | SplatStory.storyName = 'Default' 73 | -------------------------------------------------------------------------------- /.storybook/stories/SpriteAnimator.stories.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Setup } from '../Setup' 3 | import { Meta } from '@storybook/html' 4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 5 | import { SpriteAnimator, SpriteAnimatorType } from '../../src/core/SpriteAnimator' 6 | import { GUI } from 'lil-gui' 7 | 8 | export default { 9 | title: 'Misc/SpriteAnimator', 10 | } as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS 11 | let gui: GUI 12 | const allSpriteAnimators: SpriteAnimatorType[] = [] 13 | 14 | export const SpriteAnimatorStory = async () => { 15 | gui = new GUI({ title: 'Grid Story', closeFolders: true }) 16 | const { renderer, scene, camera, render } = Setup() 17 | renderer.shadowMap.enabled = true 18 | camera.position.set(5, 5, 5) 19 | const controls = new OrbitControls(camera, renderer.domElement) 20 | controls.target.set(0, 1, 0) 21 | controls.update() 22 | 23 | const floor = new THREE.Mesh( 24 | new THREE.PlaneGeometry(60, 60).rotateX(-Math.PI / 2), 25 | new THREE.ShadowMaterial({ opacity: 0.5, transparent: true, depthWrite: false, depthTest: true }) 26 | ) 27 | floor.receiveShadow = true 28 | scene.add(floor) 29 | 30 | const dirLight = new THREE.DirectionalLight(0xabcdef, 5) 31 | dirLight.position.set(15, 15, 15) 32 | dirLight.castShadow = true 33 | dirLight.shadow.mapSize.width = 1024 34 | dirLight.shadow.mapSize.height = 1024 35 | const size = 5 36 | dirLight.shadow.camera.top = size 37 | dirLight.shadow.camera.bottom = -size 38 | dirLight.shadow.camera.left = -size 39 | dirLight.shadow.camera.right = size 40 | scene.add(dirLight) 41 | scene.add(new THREE.AmbientLight(0xffffff, 0.1)) 42 | 43 | scene.add(new THREE.GridHelper(10)) 44 | 45 | const geometry = new THREE.TorusKnotGeometry(0.2, 0.1, 100, 32) 46 | const mat = new THREE.MeshStandardMaterial({ 47 | roughness: 0, 48 | }) 49 | mat.color.setHSL(Math.random(), 1, 0.5) 50 | const torusMesh = new THREE.Mesh(geometry, mat) 51 | torusMesh.position.set(0, 0.4, 0) 52 | torusMesh.castShadow = true 53 | torusMesh.receiveShadow = true 54 | scene.add(torusMesh) 55 | 56 | const folder = gui.addFolder('Light Settings') 57 | folder.add(dirLight, 'intensity', 0, 5) 58 | folder.addColor(dirLight, 'color') 59 | folder.add(dirLight.position, 'x', -15, 15).name('position x') 60 | folder.add(dirLight.position, 'y', -15, 15).name('position y') 61 | folder.add(dirLight.position, 'z', -15, 15).name('position z') 62 | 63 | await setupSprites(scene) 64 | await setupPlaneSprites(scene) 65 | 66 | render((time) => { 67 | controls.update() 68 | for (const spriteAnimator of allSpriteAnimators) { 69 | spriteAnimator.update() 70 | } 71 | }) 72 | } 73 | 74 | const setupSprites = async (scene: THREE.Scene) => { 75 | // Flame 76 | const FlameSpriteAnimator = SpriteAnimator({ 77 | startFrame: 0, 78 | fps: 40, 79 | autoPlay: true, 80 | loop: true, 81 | textureImageURL: './sprites/flame.png', 82 | textureDataURL: './sprites/flame.json', 83 | alphaTest: 0.01, 84 | }) 85 | await FlameSpriteAnimator.init() 86 | FlameSpriteAnimator.group.position.set(-1, 0.5, 2) 87 | scene.add(FlameSpriteAnimator.group) 88 | allSpriteAnimators.push(FlameSpriteAnimator) 89 | 90 | createSpriteGui('Flame', FlameSpriteAnimator) 91 | 92 | // Alien 93 | const AlienSpriteAnimator = SpriteAnimator({ 94 | startFrame: 0, 95 | autoPlay: true, 96 | loop: true, 97 | numberOfFrames: 16, 98 | alphaTest: 0.01, 99 | textureImageURL: './sprites/alien.png', 100 | }) 101 | await AlienSpriteAnimator.init() 102 | 103 | AlienSpriteAnimator.group.position.set(0, 0.5, 2) 104 | 105 | scene.add(AlienSpriteAnimator.group) 106 | createSpriteGui('Alien', AlienSpriteAnimator) 107 | 108 | allSpriteAnimators.push(AlienSpriteAnimator) 109 | 110 | // Boy 111 | const animNames = ['idle', 'celebration'] 112 | 113 | const boySA = SpriteAnimator({ 114 | // onLoopEnd={onEnd} 115 | frameName: 'idle', 116 | fps: 24, 117 | animationNames: animNames, 118 | autoPlay: true, 119 | loop: true, 120 | alphaTest: 0.01, 121 | textureImageURL: './sprites/boy_hash.png', 122 | textureDataURL: './sprites/boy_hash.json', 123 | }) 124 | await boySA.init() 125 | boySA.group.position.set(1, 0.5, 2) 126 | 127 | scene.add(boySA.group) 128 | 129 | allSpriteAnimators.push(boySA) 130 | 131 | createSpriteGui('Boy', boySA, animNames) 132 | } 133 | 134 | const setupPlaneSprites = async (scene: THREE.Scene) => { 135 | // Flame 136 | const FlameSpriteAnimator = SpriteAnimator({ 137 | startFrame: 0, 138 | fps: 40, 139 | autoPlay: true, 140 | loop: true, 141 | textureImageURL: './sprites/flame.png', 142 | textureDataURL: './sprites/flame.json', 143 | alphaTest: 0.01, 144 | asSprite: false, 145 | }) 146 | await FlameSpriteAnimator.init() 147 | FlameSpriteAnimator.group.position.set(-1, 0.5, -2) 148 | scene.add(FlameSpriteAnimator.group) 149 | allSpriteAnimators.push(FlameSpriteAnimator) 150 | 151 | createSpriteGui('Flame Plane', FlameSpriteAnimator) 152 | 153 | // Alien 154 | const AlienSpriteAnimator = SpriteAnimator({ 155 | startFrame: 0, 156 | autoPlay: true, 157 | loop: true, 158 | numberOfFrames: 16, 159 | alphaTest: 0.01, 160 | textureImageURL: './sprites/alien.png', 161 | asSprite: false, 162 | }) 163 | await AlienSpriteAnimator.init() 164 | 165 | AlienSpriteAnimator.group.position.set(0, 0.5, -2) 166 | 167 | scene.add(AlienSpriteAnimator.group) 168 | createSpriteGui('Alien Plane', AlienSpriteAnimator) 169 | 170 | allSpriteAnimators.push(AlienSpriteAnimator) 171 | 172 | // Boy 173 | const animNames = ['idle', 'celebration'] 174 | 175 | const boySA = SpriteAnimator({ 176 | // onLoopEnd={onEnd} 177 | frameName: 'idle', 178 | fps: 24, 179 | animationNames: animNames, 180 | autoPlay: true, 181 | loop: true, 182 | alphaTest: 0.01, 183 | textureImageURL: './sprites/boy_hash.png', 184 | textureDataURL: './sprites/boy_hash.json', 185 | asSprite: false, 186 | }) 187 | await boySA.init() 188 | boySA.group.position.set(1, 0.5, -2) 189 | 190 | scene.add(boySA.group) 191 | 192 | allSpriteAnimators.push(boySA) 193 | 194 | createSpriteGui('Boy Plane', boySA, animNames) 195 | } 196 | 197 | function createSpriteGui(name: string, spriteAnimator: SpriteAnimatorType, animationNames: string[] = []) { 198 | const fol = gui.addFolder(name) 199 | fol.add(spriteAnimator, 'pauseAnimation') 200 | fol.add(spriteAnimator, 'playAnimation') 201 | 202 | for (const name of animationNames) { 203 | const anim = { 204 | playAnim: () => { 205 | spriteAnimator.setFrameName(name) 206 | }, 207 | } 208 | fol 209 | .add(anim, 'playAnim') 210 | .name('play: ' + name) 211 | .onChange(() => { 212 | spriteAnimator.setFrameName(name) 213 | }) 214 | } 215 | } 216 | 217 | SpriteAnimatorStory.storyName = 'Default' 218 | -------------------------------------------------------------------------------- /.storybook/stories/shaderMaterial.stories.ts: -------------------------------------------------------------------------------- 1 | import { BoxGeometry, Mesh, Texture, TextureLoader, REVISION } from 'three' 2 | import { shaderMaterial } from '../../src/core/shaderMaterial' 3 | import { Setup } from '../Setup' 4 | import { Meta } from '@storybook/html' 5 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 6 | import { GUI } from 'lil-gui' 7 | 8 | export default { 9 | title: 'Shaders/shaderMaterial', 10 | } as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS 11 | 12 | const MyMaterial = shaderMaterial( 13 | { 14 | map: new Texture(), 15 | repeats: 1, 16 | }, 17 | /* glsl */ ` 18 | varying vec2 vUv; 19 | 20 | void main() { 21 | vUv = uv; 22 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 23 | } 24 | `, 25 | /* glsl */ ` 26 | varying vec2 vUv; 27 | uniform float repeats; 28 | uniform sampler2D map; 29 | 30 | // float random(vec2 st) { 31 | // return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123); 32 | // } 33 | 34 | void main() { 35 | vec2 uv = fract(vUv * repeats); 36 | 37 | vec3 color = vec3( 38 | texture(map, uv).r, 39 | texture(map, uv + vec2(0.01)).g, 40 | texture(map, uv - vec2(0.01)).b 41 | ); 42 | 43 | gl_FragColor = vec4(color, 1.0); 44 | 45 | #include 46 | #include <${parseInt(REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> 47 | } 48 | ` 49 | ) 50 | 51 | export const ShaderMaterialStory = async () => { 52 | const params = { 53 | repeats: 2, 54 | } 55 | const gui = new GUI({ title: ShaderMaterialStory.storyName }) 56 | const { renderer, scene, camera, render } = Setup() 57 | const controls = new OrbitControls(camera, renderer.domElement) 58 | controls.enableDamping = true 59 | 60 | const texture = new TextureLoader().load('photo-1678043639454-dbdf877f0ae8.jpeg') 61 | 62 | const geometry = new BoxGeometry(1, 1, 1) 63 | const material = new MyMaterial({ map: texture }) 64 | const mesh = new Mesh(geometry, material) 65 | scene.add(mesh) 66 | 67 | render((time) => { 68 | controls.update() 69 | mesh.rotation.x = time / 5000 70 | mesh.rotation.y = time / 2500 71 | }) 72 | material.repeats = params.repeats 73 | 74 | const folder = gui.addFolder('Settings') 75 | folder.add(params, 'repeats', 1, 5, 1).onChange((v) => { 76 | material.repeats = v 77 | }) 78 | } 79 | 80 | ShaderMaterialStory.storyName = 'Default' 81 | -------------------------------------------------------------------------------- /.storybook/stories/volumetricSpotlight.stories.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ACESFilmicToneMapping, 3 | AmbientLight, 4 | BoxGeometry, 5 | CylinderGeometry, 6 | DepthFormat, 7 | DepthTexture, 8 | Group, 9 | HalfFloatType, 10 | LinearFilter, 11 | MathUtils, 12 | Mesh, 13 | MeshStandardMaterial, 14 | PlaneGeometry, 15 | SphereGeometry, 16 | SpotLight, 17 | SpotLightHelper, 18 | UnsignedShortType, 19 | Vector2, 20 | Vector3, 21 | WebGLRenderTarget, 22 | } from 'three' 23 | import { Setup } from '../Setup' 24 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 25 | import { Meta } from '@storybook/html' 26 | import GUI from 'lil-gui' 27 | 28 | // imports 29 | import { SpotLightMaterial } from '../../src/materials/SpotLightMaterial' 30 | 31 | export default { 32 | title: 'Shaders/volumetricSpotlight', 33 | } as Meta 34 | 35 | let spotLight: SpotLight, spotLightHelper: SpotLightHelper, gui: GUI 36 | 37 | let volumeMaterial: InstanceType, 38 | volumeMesh: Mesh, 39 | depthTexture: DepthTexture, 40 | depthTarget: WebGLRenderTarget 41 | 42 | const { renderer, scene, camera, render } = Setup() 43 | 44 | const rendererSize = new Vector2() // re-usable vector to store viewport resolution 45 | 46 | const volumeParams = { 47 | radiusTop: 0.1, 48 | helper: false, 49 | useDepth: false, 50 | depthResolution: 1024, 51 | } 52 | 53 | export const VolumetricSpotlightStory = async () => { 54 | gui = new GUI({ title: VolumetricSpotlightStory.storyName }) 55 | 56 | renderer.shadowMap.enabled = true 57 | renderer.toneMapping = ACESFilmicToneMapping 58 | 59 | camera.position.set(5, 5, 5) 60 | new OrbitControls(camera, renderer.domElement) 61 | 62 | scene.add(new AmbientLight(0x666666)) 63 | 64 | distributeRandomMeshes() 65 | 66 | setupSpotlight() 67 | 68 | // render((time) => {}) 69 | } 70 | 71 | /** 72 | * Setup a volumetric spotlight 73 | */ 74 | function setupSpotlight() { 75 | spotLight = new SpotLight(0xabcdef, 500) 76 | spotLight.position.set(1, 4, 1) 77 | spotLight.angle = Math.PI / 6 78 | spotLight.distance = 10 79 | spotLight.penumbra = 0.5 80 | spotLight.castShadow = true 81 | spotLight.shadow.mapSize.width = 1024 82 | spotLight.shadow.mapSize.height = 1024 83 | 84 | scene.add(spotLight) 85 | 86 | spotLightHelper = new SpotLightHelper(spotLight) 87 | spotLightHelper.visible = false 88 | scene.add(spotLightHelper) 89 | 90 | // volume 91 | 92 | volumeMaterial = new SpotLightMaterial() 93 | volumeMaterial.attenuation = spotLight.distance 94 | volumeMaterial.cameraNear = camera.near 95 | volumeMaterial.cameraFar = camera.far 96 | 97 | volumeMesh = new Mesh() 98 | volumeMesh.material = volumeMaterial // new MeshBasicMaterial({ wireframe: true, transparent: true, opacity: 0.2 }) 99 | 100 | updateVolumeGeometry() 101 | spotLight.add(volumeMesh) 102 | 103 | const worldPosition = new Vector3() 104 | render((time) => { 105 | // make spotlight go in a circle 106 | spotLight.position.x = Math.sin(time * 0.0001) * 2 107 | spotLight.position.z = Math.cos(time * 0.0001) * 2 108 | 109 | // copy spotlight world properties to the volume mesh 110 | volumeMaterial.spotPosition.copy(volumeMesh.getWorldPosition(worldPosition)) 111 | volumeMesh.lookAt(spotLight.target.getWorldPosition(worldPosition)) 112 | 113 | // render depth if enabled 114 | if (volumeParams.useDepth) { 115 | renderDepth() 116 | } 117 | 118 | if (spotLightHelper.visible) { 119 | spotLightHelper.update() 120 | } 121 | }) 122 | 123 | addSpotLightGui(gui) 124 | } 125 | 126 | /** 127 | * Create depthTexture and DepthRender target 128 | */ 129 | function updateDepthTargets() { 130 | if (depthTexture) depthTexture.dispose() 131 | depthTexture = new DepthTexture(volumeParams.depthResolution, volumeParams.depthResolution) 132 | depthTexture.format = DepthFormat 133 | depthTexture.type = UnsignedShortType 134 | depthTexture.name = 'Depth_Buffer' 135 | 136 | if (depthTarget) depthTarget.dispose() 137 | depthTarget = new WebGLRenderTarget(volumeParams.depthResolution, volumeParams.depthResolution, { 138 | minFilter: LinearFilter, 139 | magFilter: LinearFilter, 140 | type: HalfFloatType, 141 | depthTexture, 142 | samples: 0, 143 | }) 144 | 145 | if (volumeParams.useDepth) { 146 | volumeMaterial.depth = depthTexture 147 | depthOnResize() 148 | window.addEventListener('resize', depthOnResize) 149 | } else { 150 | volumeMaterial.depth = null 151 | window.removeEventListener('resize', depthOnResize) 152 | volumeMaterial.resolution.set(0, 0) 153 | } 154 | } 155 | 156 | /** 157 | * Render depth data 158 | */ 159 | function renderDepth() { 160 | volumeMaterial.depth = null 161 | renderer.setRenderTarget(depthTarget) 162 | renderer.render(scene, camera) 163 | renderer.setRenderTarget(null) 164 | volumeMaterial.depth = depthTexture 165 | } 166 | 167 | /** 168 | * If volumeMaterial.resolution and viewport resolution need to stay in sync to prevent alignment issues 169 | */ 170 | function depthOnResize() { 171 | renderer.getSize(rendererSize) 172 | rendererSize.multiplyScalar(renderer.getPixelRatio()) // depth texture will get misaligned if pixel is not multiplied 173 | volumeMaterial.resolution.copy(rendererSize) 174 | } 175 | 176 | /** 177 | * Distribute random spheres, and tall boxes & add ground 178 | */ 179 | function distributeRandomMeshes() { 180 | // sphereGroup 181 | const sphereGroup = new Group() 182 | const sphereRadius = 0.3 183 | const geometry = new SphereGeometry(sphereRadius).translate(0, sphereRadius, 0) 184 | 185 | for (let i = 0; i < 10; i++) { 186 | const material = new MeshStandardMaterial({ 187 | color: Math.random() * 0xffffff, 188 | roughness: Math.random(), 189 | }) 190 | const sphere = new Mesh(geometry, material) 191 | sphere.position.x = MathUtils.randFloatSpread(6) 192 | sphere.position.y = MathUtils.randFloat(0, 3) 193 | sphere.position.z = MathUtils.randFloatSpread(6) 194 | sphere.castShadow = true 195 | sphere.receiveShadow = true 196 | sphereGroup.add(sphere) 197 | } 198 | scene.add(sphereGroup) 199 | 200 | // ground 201 | const groundMaterial = new MeshStandardMaterial({ color: 0x404040 }) 202 | 203 | const ground = new Mesh(new PlaneGeometry(20000, 20000, 8, 8), groundMaterial) 204 | ground.rotation.x = -Math.PI / 2 205 | ground.receiveShadow = true 206 | scene.add(ground) 207 | 208 | // columns 209 | const boxGeometry = new BoxGeometry(0.2, 1, 0.2).translate(0, 0.5, 0) 210 | for (let i = 0; i < 40; i++) { 211 | const column = new Mesh(boxGeometry, groundMaterial) 212 | column.castShadow = true 213 | column.receiveShadow = true 214 | scene.add(column) 215 | column.scale.y = MathUtils.randFloat(0.1, 3) 216 | column.position.set(MathUtils.randFloatSpread(5), 0, MathUtils.randFloatSpread(5)) 217 | } 218 | } 219 | 220 | /** 221 | * Add gui for controls 222 | * @param gui 223 | */ 224 | function addSpotLightGui(gui: GUI) { 225 | const folder = gui.addFolder('SpotLight options') 226 | folder.addColor(spotLight, 'color').onChange(() => { 227 | spotLightHelper.update() 228 | volumeMaterial.lightColor.copy(spotLight.color) 229 | }) 230 | 231 | folder.add(volumeParams, 'helper').onChange((v) => { 232 | spotLightHelper.visible = v 233 | }) 234 | 235 | folder.add(spotLight, 'intensity', 0, 2000) 236 | 237 | folder.add(spotLight, 'penumbra', 0, 1) 238 | 239 | folder.add(spotLight, 'angle', 0, Math.PI / 2).onChange(() => { 240 | spotLightHelper.update() 241 | updateVolumeGeometry() 242 | }) 243 | 244 | folder.add(spotLight, 'distance', 1, 10).onChange(() => { 245 | spotLightHelper.update() 246 | updateVolumeGeometry() 247 | }) 248 | 249 | const volFolder = gui.addFolder('Volume options') 250 | 251 | volFolder.add(volumeMaterial, 'opacity', 0, 2) 252 | volFolder.add(volumeMaterial, 'attenuation', 0, 10) 253 | volFolder.add(volumeMaterial, 'anglePower', 0, 24) 254 | volFolder.add(volumeParams, 'radiusTop', 0, 1).onChange(() => { 255 | updateVolumeGeometry() 256 | }) 257 | 258 | volFolder.add(volumeParams, 'useDepth').onChange(updateDepthTargets) 259 | volFolder.add(volumeParams, 'depthResolution', 128, 2048, 128).onChange(updateDepthTargets) 260 | } 261 | 262 | /** 263 | * Update Volume mesh geometry so it's aligned with the spotlight cone 264 | */ 265 | function updateVolumeGeometry() { 266 | const distance = spotLight.distance 267 | const radiusBottom = Math.tan(spotLight.angle) * spotLight.distance 268 | const radiusTop = volumeParams.radiusTop 269 | 270 | const geometry = new CylinderGeometry(radiusTop, radiusBottom, distance, 128, 64, true) 271 | geometry.translate(0, -distance / 2, 0) 272 | geometry.rotateX(-Math.PI / 2) 273 | 274 | volumeMesh.geometry?.dispose() // dispose old geometry 275 | volumeMesh.geometry = geometry 276 | } 277 | 278 | VolumetricSpotlightStory.storyName = 'VolumetricSpotlight' 279 | -------------------------------------------------------------------------------- /.storybook/theme.ts: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming/create' 2 | import dreiLogo from '../logo.jpg' 3 | 4 | export default create({ 5 | base: 'light', 6 | brandImage: dreiLogo, 7 | appBg: 'white', 8 | }) 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at team@react-spring.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for wanting to make a contribution and wanting to improve this library for everyone! This repository uses Typescript so please continue to do so, you can always reach out in the repo or the [discord](https://pmnd.rs/discord). This is a guideline, use your initiative, if you don't think it makes sense to do a step in here, don't bother it's normally okay. we're chill. 4 | 5 | ## How to Contribute 6 | 7 | 1. Fork and clone the repo 8 | 2. Run `yarn install` to install dependencies 9 | 3. Create a branch for your PR with `git checkout -b pr-type/issue-number-your-branch-name` 10 | 4. Let's get cooking! 👨🏻‍🍳🥓 11 | 12 | ## Commit Guidelines 13 | 14 | Be sure your commit messages follow this specification: https://www.conventionalcommits.org/en/v1.0.0-beta.4/ 15 | 16 | ## Publishing 17 | 18 | We use `semantic-release-action` to deploy the package. Because of this only certain commits will trigger the action of creating a release: 19 | 20 | - `fix:` will create a `0.0.x` version 21 | - `feat:` will create a `0.x.0` version 22 | - `BREAKING CHANGE:` will create a `x.0.0` version 23 | 24 | We release on `master`, `beta` & `alpha`. `beta` & `alpha` are configured to be prerelease. Any other commits will not fire a release. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Poimandres 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 | -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/drei-vanilla/fb0765741897c70f13589950495917fa4f5d15a0/logo.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pmndrs/vanilla", 3 | "version": "0.0.0-semantic-release", 4 | "private": true, 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "description": "drei-inspired helpers for threejs", 9 | "keywords": [ 10 | "drei", 11 | "three", 12 | "threejs" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/pmndrs/drei-vanilla.git" 17 | }, 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/pmndrs/drei-vanilla/issues" 21 | }, 22 | "homepage": "https://github.com/pmndrs/drei-vanilla", 23 | "maintainers": [ 24 | "VishaL (https://github.com/vis-prime)", 25 | "Paul Henschel (https://github.com/drcmda)", 26 | "Gianmarco Simone (https://github.com/gsimone)", 27 | "Marco Perego (https://github.com/emmelleppi)", 28 | "Josh Ellis (https://github.com/joshuaellis)", 29 | "Antoine BERNIER (https://github.com/abernier)" 30 | ], 31 | "main": "index.cjs.js", 32 | "module": "index.js", 33 | "types": "index.d.ts", 34 | "sideEffects": false, 35 | "commitlint": { 36 | "extends": [ 37 | "@commitlint/config-conventional" 38 | ], 39 | "rules": { 40 | "body-max-line-length": [ 41 | 0 42 | ] 43 | } 44 | }, 45 | "scripts": { 46 | "prebuild": "rimraf dist && npm run typegen", 47 | "build": "rollup -c && npm run copy", 48 | "prepare": "npm run build && husky install", 49 | "eslint": "eslint --fix .", 50 | "eslint:ci": "eslint .", 51 | "prettier": "prettier --check .", 52 | "prettier-fix": "prettier --write .", 53 | "test": "npm run eslint:ci && npm run prettier", 54 | "typecheck": "tsc --noEmit --emitDeclarationOnly false --strict --jsx react", 55 | "typegen": "tsc --emitDeclarationOnly", 56 | "copy": "copyfiles package.json README.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.husky=undefined; this.prettier=undefined; this.jest=undefined; this['lint-staged']=undefined;\"", 57 | "release": "semantic-release", 58 | "storybook": "start-storybook -p 6006", 59 | "build-storybook": "build-storybook" 60 | }, 61 | "dependencies": { 62 | "glsl-noise": "^0.0.0" 63 | }, 64 | "devDependencies": { 65 | "@babel/core": "^7.14.3", 66 | "@babel/plugin-transform-modules-commonjs": "^7.14.0", 67 | "@babel/plugin-transform-runtime": "^7.14.3", 68 | "@babel/preset-env": "^7.14.2", 69 | "@babel/preset-typescript": "^7.10.4", 70 | "@babel/runtime": "^7.11.2", 71 | "@commitlint/cli": "^12.0.1", 72 | "@commitlint/config-conventional": "^12.0.1", 73 | "@rollup/plugin-babel": "^5.3.0", 74 | "@rollup/plugin-commonjs": "^19.0.0", 75 | "@rollup/plugin-json": "^4.1.0", 76 | "@rollup/plugin-node-resolve": "^13.0.0", 77 | "@semantic-release/git": "^10.0.1", 78 | "@storybook/addon-actions": "^6.5.16", 79 | "@storybook/addon-essentials": "^6.5.16", 80 | "@storybook/addon-interactions": "^6.5.16", 81 | "@storybook/addon-links": "^6.5.16", 82 | "@storybook/builder-webpack5": "^6.5.16", 83 | "@storybook/html": "^6.5.16", 84 | "@storybook/manager-webpack5": "^6.5.16", 85 | "@storybook/testing-library": "^0.0.13", 86 | "@types/jest": "^26.0.10", 87 | "@types/lodash-es": "^4.17.3", 88 | "@types/three": "^0.166.0", 89 | "@typescript-eslint/eslint-plugin": "^5.4.0", 90 | "@typescript-eslint/parser": "^5.4.0", 91 | "babel-eslint": "^10.1.0", 92 | "babel-loader": "^8.1.0", 93 | "copyfiles": "^2.3.0", 94 | "eslint": "^7.7.0", 95 | "eslint-config-prettier": "^8.3.0", 96 | "eslint-plugin-import": "^2.22.0", 97 | "eslint-plugin-prettier": "^3.1.4", 98 | "glslify-loader": "^2.0.0", 99 | "husky": "^6.0.0", 100 | "jest": "^26.4.1", 101 | "json": "^11.0.0", 102 | "lil-gui": "^0.19.2", 103 | "prettier": "^2.4.1", 104 | "pretty-quick": "^3.1.0", 105 | "raw-loader": "^4.0.2", 106 | "react": "^18.0.0", 107 | "react-dom": "^18.0.0", 108 | "rimraf": "^3.0.2", 109 | "rollup": "^2.78.1", 110 | "rollup-plugin-glslify": "^1.3.0", 111 | "rollup-plugin-multi-input": "^1.3.1", 112 | "rollup-plugin-terser": "^7.0.2", 113 | "semantic-release": "^20.1.1", 114 | "three": "^0.166.1", 115 | "ts-node": "^10.9.2", 116 | "typescript": "^5.5.3", 117 | "yarn": "^1.22.17" 118 | }, 119 | "peerDependencies": { 120 | "three": ">=0.137" 121 | }, 122 | "packageManager": "yarn@1.22.22" 123 | } 124 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ['main'], 3 | plugins: [ 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | [ 7 | '@semantic-release/npm', 8 | { 9 | pkgRoot: './dist', 10 | }, 11 | ], 12 | '@semantic-release/github', 13 | [ 14 | '@semantic-release/git', 15 | { 16 | assets: ['package.json'], 17 | message: 'chore(release): ${nextRelease.version}', 18 | }, 19 | ], 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import babel from '@rollup/plugin-babel' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import json from '@rollup/plugin-json' 5 | import glslify from 'rollup-plugin-glslify' 6 | import multiInput from 'rollup-plugin-multi-input' 7 | import { terser } from 'rollup-plugin-terser' 8 | 9 | const root = process.platform === 'win32' ? path.resolve('/') : '/' 10 | const external = (id) => !id.startsWith('.') && !id.startsWith(root) 11 | const extensions = ['.js', '.jsx', '.ts', '.tsx', '.json'] 12 | 13 | const getBabelOptions = ({ useESModules }) => ({ 14 | babelrc: false, 15 | extensions, 16 | exclude: '**/node_modules/**', 17 | babelHelpers: 'runtime', 18 | presets: [ 19 | [ 20 | '@babel/preset-env', 21 | { 22 | include: [ 23 | '@babel/plugin-proposal-optional-chaining', 24 | '@babel/plugin-proposal-nullish-coalescing-operator', 25 | '@babel/plugin-proposal-numeric-separator', 26 | '@babel/plugin-proposal-logical-assignment-operators', 27 | ], 28 | bugfixes: true, 29 | loose: true, 30 | modules: false, 31 | targets: '> 1%, not dead, not ie 11, not op_mini all', 32 | }, 33 | ], 34 | '@babel/preset-typescript', 35 | ], 36 | plugins: [ 37 | '@babel/plugin-proposal-nullish-coalescing-operator', 38 | ['@babel/transform-runtime', { regenerator: false, useESModules }], 39 | ], 40 | }) 41 | 42 | export default [ 43 | { 44 | input: ['src/**/*.ts', 'src/**/*.tsx', '!src/index.ts'], 45 | output: { dir: `dist`, format: 'esm' }, 46 | external, 47 | plugins: [ 48 | multiInput(), 49 | json(), 50 | glslify(), 51 | babel(getBabelOptions({ useESModules: true }, '>1%, not dead, not ie 11, not op_mini all')), 52 | resolve({ extensions }), 53 | ], 54 | }, 55 | { 56 | input: `./src/index.ts`, 57 | output: { dir: `dist`, format: 'esm' }, 58 | external, 59 | plugins: [ 60 | json(), 61 | glslify(), 62 | babel(getBabelOptions({ useESModules: true }, '>1%, not dead, not ie 11, not op_mini all')), 63 | resolve({ extensions }), 64 | ], 65 | preserveModules: true, 66 | }, 67 | { 68 | input: ['src/**/*.ts', 'src/**/*.tsx', '!src/index.ts'], 69 | output: { dir: `dist`, format: 'cjs' }, 70 | external, 71 | plugins: [ 72 | multiInput({ 73 | transformOutputPath: (output) => output.replace(/\.[^/.]+$/, '.cjs.js'), 74 | }), 75 | json(), 76 | glslify(), 77 | babel(getBabelOptions({ useESModules: false })), 78 | resolve({ extensions }), 79 | terser(), 80 | ], 81 | }, 82 | { 83 | input: `./src/index.ts`, 84 | output: { file: `dist/index.cjs.js`, format: 'cjs' }, 85 | external, 86 | plugins: [json(), glslify(), babel(getBabelOptions({ useESModules: false })), resolve({ extensions }), terser()], 87 | }, 88 | ] 89 | -------------------------------------------------------------------------------- /src/core/AccumulativeShadows.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { shaderMaterial } from './shaderMaterial' 3 | import { MeshDiscardMaterial } from '../materials/MeshDiscardMaterial' 4 | 5 | function isLight(object: any): object is THREE.Light { 6 | return object.isLight 7 | } 8 | 9 | function isGeometry(object: any): object is THREE.Mesh { 10 | return !!object.geometry 11 | } 12 | 13 | type SoftShadowMaterialProps = { 14 | map: THREE.Texture | null 15 | color: THREE.Color 16 | alphaTest: number 17 | opacity: number 18 | blend: number 19 | } 20 | 21 | const SoftShadowMaterial = shaderMaterial( 22 | { 23 | color: new THREE.Color(0x000000), 24 | blend: 2.0, 25 | alphaTest: 0.75, 26 | opacity: 0, 27 | map: null, 28 | }, 29 | `varying vec2 vUv; 30 | void main() { 31 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.); 32 | vUv = uv; 33 | }`, 34 | `varying vec2 vUv; 35 | uniform sampler2D map; 36 | uniform vec3 color; 37 | uniform float opacity; 38 | uniform float alphaTest; 39 | uniform float blend; 40 | void main() { 41 | vec4 sampledDiffuseColor = texture2D(map, vUv); 42 | gl_FragColor = vec4(color * sampledDiffuseColor.r * blend, max(0.0, (1.0 - (sampledDiffuseColor.r + sampledDiffuseColor.g + sampledDiffuseColor.b) / alphaTest)) * opacity); 43 | #include 44 | #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> 45 | }` 46 | ) 47 | 48 | // Based on "Progressive Light Map Accumulator", by [zalo](https://github.com/zalo/) 49 | class ProgressiveLightMap { 50 | renderer: THREE.WebGLRenderer 51 | res: number 52 | scene: THREE.Scene 53 | object: THREE.Mesh | null 54 | buffer1Active: boolean 55 | progressiveLightMap1: THREE.WebGLRenderTarget 56 | progressiveLightMap2: THREE.WebGLRenderTarget 57 | discardMat: THREE.ShaderMaterial 58 | targetMat: THREE.MeshLambertMaterial 59 | previousShadowMap: { value: THREE.Texture } 60 | averagingWindow: { value: number } 61 | clearColor: THREE.Color 62 | clearAlpha: number 63 | lights: { object: THREE.Light; intensity: number }[] 64 | meshes: { object: THREE.Mesh; material: THREE.Material | THREE.Material[] }[] 65 | 66 | constructor(renderer: THREE.WebGLRenderer, scene: THREE.Scene, res: number = 1024) { 67 | this.renderer = renderer 68 | this.res = res 69 | this.scene = scene 70 | this.buffer1Active = false 71 | this.lights = [] 72 | this.meshes = [] 73 | this.object = null 74 | this.clearColor = new THREE.Color() 75 | this.clearAlpha = 0 76 | 77 | // Create the Progressive LightMap Texture 78 | const format = /(Android|iPad|iPhone|iPod)/g.test(navigator.userAgent) ? THREE.HalfFloatType : THREE.FloatType 79 | this.progressiveLightMap1 = new THREE.WebGLRenderTarget(this.res, this.res, { 80 | type: format, 81 | }) 82 | this.progressiveLightMap2 = new THREE.WebGLRenderTarget(this.res, this.res, { 83 | type: format, 84 | }) 85 | 86 | // Inject some spicy new logic into a standard phong material 87 | this.discardMat = new MeshDiscardMaterial() 88 | this.targetMat = new THREE.MeshLambertMaterial({ fog: false }) 89 | this.previousShadowMap = { value: this.progressiveLightMap1.texture } 90 | this.averagingWindow = { value: 100 } 91 | this.targetMat.onBeforeCompile = (shader) => { 92 | // Vertex Shader: Set Vertex Positions to the Unwrapped UV Positions 93 | shader.vertexShader = 94 | 'varying vec2 vUv;\n' + 95 | shader.vertexShader.slice(0, -1) + 96 | 'vUv = uv; gl_Position = vec4((uv - 0.5) * 2.0, 1.0, 1.0); }' 97 | 98 | // Fragment Shader: Set Pixels to average in the Previous frame's Shadows 99 | const bodyStart = shader.fragmentShader.indexOf('void main() {') 100 | shader.fragmentShader = 101 | 'varying vec2 vUv;\n' + 102 | shader.fragmentShader.slice(0, bodyStart) + 103 | 'uniform sampler2D previousShadowMap;\n uniform float averagingWindow;\n' + 104 | shader.fragmentShader.slice(bodyStart - 1, -1) + 105 | `\nvec3 texelOld = texture2D(previousShadowMap, vUv).rgb; 106 | gl_FragColor.rgb = mix(texelOld, gl_FragColor.rgb, 1.0/ averagingWindow); 107 | }` 108 | 109 | // Set the Previous Frame's Texture Buffer and Averaging Window 110 | shader.uniforms.previousShadowMap = this.previousShadowMap 111 | shader.uniforms.averagingWindow = this.averagingWindow 112 | } 113 | } 114 | 115 | clear() { 116 | this.renderer.getClearColor(this.clearColor) 117 | this.clearAlpha = this.renderer.getClearAlpha() 118 | this.renderer.setClearColor('black', 1) 119 | this.renderer.setRenderTarget(this.progressiveLightMap1) 120 | this.renderer.clear() 121 | this.renderer.setRenderTarget(this.progressiveLightMap2) 122 | this.renderer.clear() 123 | this.renderer.setRenderTarget(null) 124 | this.renderer.setClearColor(this.clearColor, this.clearAlpha) 125 | 126 | this.lights = [] 127 | this.meshes = [] 128 | 129 | this.scene.traverse((object) => { 130 | if (isGeometry(object)) { 131 | this.meshes.push({ object, material: object.material }) 132 | } else if (isLight(object)) { 133 | this.lights.push({ object, intensity: object.intensity }) 134 | } 135 | }) 136 | } 137 | 138 | prepare() { 139 | this.lights.forEach((light) => (light.object.intensity = 0)) 140 | this.meshes.forEach((mesh) => (mesh.object.material = this.discardMat)) 141 | } 142 | 143 | finish() { 144 | this.lights.forEach((light) => (light.object.intensity = light.intensity)) 145 | this.meshes.forEach((mesh) => (mesh.object.material = mesh.material)) 146 | } 147 | 148 | configure(object: THREE.Mesh) { 149 | this.object = object 150 | } 151 | 152 | update(camera: THREE.Camera, blendWindow = 100) { 153 | if (!this.object) return 154 | // Set each object's material to the UV Unwrapped Surface Mapping Version 155 | this.averagingWindow.value = blendWindow 156 | this.object.material = this.targetMat 157 | // Ping-pong two surface buffers for reading/writing 158 | const activeMap = this.buffer1Active ? this.progressiveLightMap1 : this.progressiveLightMap2 159 | const inactiveMap = this.buffer1Active ? this.progressiveLightMap2 : this.progressiveLightMap1 160 | // Render the object's surface maps 161 | const oldBg = this.scene.background 162 | this.scene.background = null 163 | this.renderer.setRenderTarget(activeMap) 164 | this.previousShadowMap.value = inactiveMap.texture 165 | this.buffer1Active = !this.buffer1Active 166 | this.renderer.render(this.scene, camera) 167 | this.renderer.setRenderTarget(null) 168 | this.scene.background = oldBg 169 | } 170 | } 171 | 172 | export { SoftShadowMaterial, ProgressiveLightMap } 173 | -------------------------------------------------------------------------------- /src/core/Billboard.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | export type BillboardProps = { 4 | /** 5 | * @default true 6 | */ 7 | follow?: boolean 8 | /** 9 | * @default false 10 | */ 11 | lockX?: boolean 12 | /** 13 | * @default false 14 | */ 15 | lockY?: boolean 16 | /** 17 | * @default false 18 | */ 19 | lockZ?: boolean 20 | } 21 | 22 | export type BillboardType = { 23 | group: THREE.Group 24 | /** 25 | * Should called every frame to update the billboard 26 | */ 27 | update: (camera: THREE.Camera) => void 28 | updateProps: (newProps: Partial) => void 29 | } 30 | 31 | export const Billboard = ({ 32 | follow = true, 33 | lockX = false, 34 | lockY = false, 35 | lockZ = false, 36 | }: BillboardProps = {}): BillboardType => { 37 | const group = new THREE.Group() 38 | 39 | const props: BillboardProps = { 40 | follow, 41 | lockX, 42 | lockY, 43 | lockZ, 44 | } 45 | 46 | function update(camera: THREE.Camera) { 47 | const { follow, lockX, lockY, lockZ } = props 48 | if (!follow) return 49 | // save previous rotation in case we're locking an axis 50 | const prevRotation = group.rotation.clone() 51 | 52 | // always face the camera 53 | camera.getWorldQuaternion(group.quaternion) 54 | 55 | // readjust any axis that is locked 56 | if (lockX) group.rotation.x = prevRotation.x 57 | if (lockY) group.rotation.y = prevRotation.y 58 | if (lockZ) group.rotation.z = prevRotation.z 59 | } 60 | 61 | return { 62 | group, 63 | update, 64 | updateProps(newProps) { 65 | Object.assign(props, newProps) 66 | }, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/core/Cloud.ts: -------------------------------------------------------------------------------- 1 | import { 2 | REVISION, 3 | DynamicDrawUsage, 4 | Color, 5 | Group, 6 | Texture, 7 | Vector3, 8 | InstancedMesh, 9 | Material, 10 | MeshLambertMaterial, 11 | Matrix4, 12 | Quaternion, 13 | PlaneGeometry, 14 | InstancedBufferAttribute, 15 | BufferAttribute, 16 | Camera, 17 | } from 'three' 18 | import { setUpdateRange } from '../../src/helpers/deprecated' 19 | 20 | export const CLOUD_URL = 21 | 'https://rawcdn.githack.com/pmndrs/drei-assets/9225a9f1fbd449d9411125c2f419b843d0308c9f/cloud.png' 22 | 23 | type CloudState = { 24 | ref: Group 25 | uuid: string 26 | index: number 27 | segments: number 28 | dist: number 29 | matrix: Matrix4 30 | bounds: Vector3 31 | position: Vector3 32 | volume: number 33 | length: number 34 | speed: number 35 | growth: number 36 | opacity: number 37 | fade: number 38 | density: number 39 | rotation: number 40 | rotationFactor: number 41 | color: Color 42 | } 43 | 44 | type CloudsProps = { 45 | /** cloud texture*/ 46 | texture?: Texture | undefined 47 | /** Maximum number of segments, default: 200 (make this tight to save memory!) */ 48 | limit?: number 49 | /** How many segments it renders, default: undefined (all) */ 50 | range?: number 51 | /** Which material it will override, default: MeshLambertMaterial */ 52 | material?: typeof Material 53 | /** Frustum culling, default: true */ 54 | frustumCulled?: boolean 55 | } 56 | 57 | type CloudProps = { 58 | /** A seeded random will show the same cloud consistently, default: Math.random() */ 59 | seed?: number 60 | /** How many segments or particles the cloud will have, default: 20 */ 61 | segments?: number 62 | /** The box3 bounds of the cloud, default: [5, 1, 1] */ 63 | bounds?: Vector3 64 | /** How to arrange segment volume inside the bounds, default: inside (cloud are smaller at the edges) */ 65 | concentrate?: 'random' | 'inside' | 'outside' 66 | /** The general scale of the segments */ 67 | scale?: Vector3 68 | /** The volume/thickness of the segments, default: 6 */ 69 | volume?: number 70 | /** The smallest volume when distributing clouds, default: 0.25 */ 71 | smallestVolume?: number 72 | /** An optional function that allows you to distribute points and volumes (overriding all settings), default: null 73 | * Both point and volume are factors, point x/y/z can be between -1 and 1, volume between 0 and 1 */ 74 | distribute?: ((cloud: CloudState, index: number) => { point: Vector3; volume?: number }) | null 75 | /** Growth factor for animated clouds (speed > 0), default: 4 */ 76 | growth?: number 77 | /** Animation factor, default: 0 */ 78 | speed?: number 79 | /** Camera distance until the segments will fade, default: 10 */ 80 | fade?: number 81 | /** Opacity, default: 1 */ 82 | opacity?: number 83 | /** Color, default: white */ 84 | color?: Color 85 | } 86 | 87 | const parentMatrix = /* @__PURE__ */ new Matrix4() 88 | const translation = /* @__PURE__ */ new Vector3() 89 | const rotation = /* @__PURE__ */ new Quaternion() 90 | const cPos = /* @__PURE__ */ new Vector3() 91 | const cQuat = /* @__PURE__ */ new Quaternion() 92 | const scale = /* @__PURE__ */ new Vector3() 93 | 94 | const CloudMaterialMaker = (material: typeof Material) => { 95 | return class extends (material as typeof Material) { 96 | map: Texture | undefined 97 | constructor() { 98 | super() 99 | const opaque_fragment = parseInt(REVISION.replace(/\D+/g, '')) >= 154 ? 'opaque_fragment' : 'output_fragment' 100 | this.onBeforeCompile = (shader) => { 101 | shader.vertexShader = 102 | `attribute float cloudOpacity; 103 | varying float vOpacity; 104 | ` + 105 | shader.vertexShader.replace( 106 | '#include ', 107 | `#include 108 | vOpacity = cloudOpacity; 109 | ` 110 | ) 111 | shader.fragmentShader = 112 | `varying float vOpacity; 113 | ` + 114 | shader.fragmentShader.replace( 115 | `#include <${opaque_fragment}>`, 116 | `#include <${opaque_fragment}> 117 | gl_FragColor = vec4(outgoingLight, diffuseColor.a * vOpacity); 118 | ` 119 | ) 120 | } 121 | } 122 | } 123 | } 124 | 125 | export class Clouds extends Group { 126 | ref: Group 127 | instance: InstancedMesh 128 | cloudMaterial: Material 129 | update: (camera: Camera, time: number, delta: number) => void 130 | 131 | constructor({ limit = 200, range, material = MeshLambertMaterial, texture, frustumCulled = true }: CloudsProps = {}) { 132 | super() 133 | this.name = 'Clouds' 134 | this.ref = this 135 | const ref = this 136 | const planeGeometry = new PlaneGeometry(1, 1) 137 | 138 | const opacities = new Float32Array(Array.from({ length: limit }, () => 1)) 139 | const colors = new Float32Array(Array.from({ length: limit }, () => [1, 1, 1]).flat()) 140 | 141 | const opAttr = new InstancedBufferAttribute(opacities, 1) 142 | opAttr.setUsage(DynamicDrawUsage) 143 | planeGeometry.setAttribute('cloudOpacity', opAttr) 144 | 145 | const CloudMaterial = CloudMaterialMaker(material) 146 | 147 | const cloudMaterial = new CloudMaterial() 148 | cloudMaterial.map = texture 149 | cloudMaterial.transparent = true 150 | cloudMaterial.depthWrite = false 151 | cloudMaterial.needsUpdate = true 152 | this.cloudMaterial = cloudMaterial 153 | 154 | this.instance = new InstancedMesh(planeGeometry, cloudMaterial, limit) 155 | const instance = this.instance 156 | 157 | instance.matrixAutoUpdate = false 158 | instance.frustumCulled = frustumCulled 159 | instance.instanceColor = new InstancedBufferAttribute(colors, 3) 160 | instance.instanceColor.setUsage(DynamicDrawUsage) 161 | 162 | ref.add(instance) 163 | 164 | const clouds: CloudState[] = [] 165 | 166 | const getCloudArray = () => { 167 | const oldCount = clouds.length 168 | let currentCount = 0 169 | for (let index = 0; index < this.ref.children.length; index++) { 170 | const mesh = this.ref.children[index] as Cloud 171 | if (!mesh.cloudStateArray) continue 172 | currentCount += mesh.cloudStateArray.length 173 | } 174 | 175 | if (oldCount === currentCount) { 176 | return clouds 177 | } 178 | 179 | clouds.length = 0 180 | for (let index = 0; index < this.ref.children.length; index++) { 181 | const mesh = this.ref.children[index] as Cloud 182 | if (!mesh.cloudStateArray) continue 183 | 184 | clouds.push(...mesh.cloudStateArray) 185 | } 186 | updateInstancedMeshDrawRange() 187 | 188 | return clouds 189 | } 190 | 191 | const updateInstancedMeshDrawRange = () => { 192 | const count = Math.min(limit, range !== undefined ? range : limit, clouds.length) 193 | instance.count = count 194 | setUpdateRange(instance.instanceMatrix, { offset: 0, count: count * 16 }) 195 | if (instance.instanceColor) { 196 | setUpdateRange(instance.instanceColor, { offset: 0, count: count * 3 }) 197 | } 198 | setUpdateRange(instance.geometry.attributes.cloudOpacity as BufferAttribute, { offset: 0, count: count }) 199 | } 200 | 201 | let t = 0 202 | let index = 0 203 | let config: CloudState 204 | const qat = new Quaternion() 205 | const dir = new Vector3(0, 0, 1) 206 | const pos = new Vector3() 207 | 208 | this.update = (camera, elapsedTime, delta) => { 209 | t = elapsedTime 210 | 211 | parentMatrix.copy(instance.matrixWorld).invert() 212 | camera.matrixWorld.decompose(cPos, cQuat, scale) 213 | 214 | const clouds = getCloudArray() 215 | 216 | for (index = 0; index < clouds.length; index++) { 217 | config = clouds[index] 218 | config.ref.matrixWorld.decompose(translation, rotation, scale) 219 | translation.add(pos.copy(config.position).applyQuaternion(rotation).multiply(scale)) 220 | rotation.copy(cQuat).multiply(qat.setFromAxisAngle(dir, (config.rotation += delta * config.rotationFactor))) 221 | scale.multiplyScalar(config.volume + ((1 + Math.sin(t * config.density * config.speed)) / 2) * config.growth) 222 | config.matrix.compose(translation, rotation, scale).premultiply(parentMatrix) 223 | config.dist = translation.distanceTo(cPos) 224 | } 225 | 226 | // Depth-sort. Instances have no specific draw order, w/o sorting z would be random 227 | clouds.sort((a, b) => b.dist - a.dist) 228 | for (index = 0; index < clouds.length; index++) { 229 | config = clouds[index] 230 | opacities[index] = config.opacity * (config.dist < config.fade - 1 ? config.dist / config.fade : 1) 231 | instance.setMatrixAt(index, config.matrix) 232 | instance.setColorAt(index, config.color) 233 | } 234 | 235 | // Update instance 236 | instance.geometry.attributes.cloudOpacity.needsUpdate = true 237 | instance.instanceMatrix.needsUpdate = true 238 | if (instance.instanceColor) instance.instanceColor.needsUpdate = true 239 | } 240 | } 241 | } 242 | 243 | let cloudCount = 0 244 | /* @__PURE__ */ 245 | export class Cloud extends Group { 246 | seed: number 247 | segments: number 248 | bounds: Vector3 249 | concentrate: string 250 | volume: number 251 | smallestVolume: number 252 | distribute: ((cloud: CloudState, index: number) => { point: Vector3; volume?: number | undefined }) | null 253 | growth: number 254 | speed: number 255 | fade: number 256 | opacity: number 257 | color: Color 258 | ref: any 259 | cloudStateArray: CloudState[] 260 | constructor({ 261 | opacity = 1, 262 | speed = 0, 263 | bounds = new Vector3().fromArray([5, 1, 1]), 264 | segments = 20, 265 | color = new Color('#ffffff'), 266 | fade = 10, 267 | volume = 6, 268 | smallestVolume = 0.25, 269 | distribute = null, 270 | growth = 4, 271 | concentrate = 'inside', 272 | seed = Math.random(), 273 | }: CloudProps = {}) { 274 | super() 275 | this.name = 'cloud_' + cloudCount++ 276 | this.seed = seed 277 | this.segments = segments 278 | this.bounds = bounds 279 | this.concentrate = concentrate 280 | this.volume = volume 281 | this.smallestVolume = smallestVolume 282 | this.distribute = distribute 283 | this.growth = growth 284 | this.speed = speed 285 | this.fade = fade 286 | this.opacity = opacity 287 | this.color = color 288 | 289 | this.ref = this 290 | 291 | this.cloudStateArray = [] 292 | this.updateCloud() 293 | } 294 | 295 | /** 296 | * @private 297 | */ 298 | updateCloudStateArray() { 299 | if (this.cloudStateArray.length === this.segments) return 300 | const { segments, uuid } = this 301 | 302 | if (this.cloudStateArray.length > this.segments) { 303 | this.cloudStateArray.splice(0, this.cloudStateArray.length - this.segments) 304 | } else { 305 | for (let index = this.cloudStateArray.length; index < segments; index++) { 306 | this.cloudStateArray.push({ 307 | segments, 308 | bounds: new Vector3(1, 1, 1), 309 | position: new Vector3(), 310 | uuid, 311 | index, 312 | ref: this, 313 | dist: 0, 314 | matrix: new Matrix4(), 315 | volume: 0, 316 | length: 0, 317 | speed: 0, 318 | growth: 0, 319 | opacity: 1, 320 | fade: 0, 321 | density: 0, 322 | rotation: index * (Math.PI / segments), 323 | rotationFactor: 0, // Add rotationFactor property 324 | color: new Color(), 325 | } as CloudState) 326 | } 327 | } 328 | } 329 | 330 | updateCloud() { 331 | const { 332 | volume, 333 | color, 334 | speed, 335 | growth, 336 | opacity, 337 | fade, 338 | bounds, 339 | seed, 340 | cloudStateArray, 341 | distribute, 342 | segments, 343 | concentrate, 344 | smallestVolume, 345 | } = this 346 | 347 | this.updateCloudStateArray() 348 | 349 | let seedInc = 0 350 | function random() { 351 | const x = Math.sin(seed + seedInc) * 10000 352 | seedInc++ 353 | 354 | return x - Math.floor(x) 355 | } 356 | 357 | cloudStateArray.forEach((cloud, index) => { 358 | // Only distribute randomly if there are multiple segments 359 | cloud.segments = segments 360 | cloud.volume = volume 361 | cloud.color = color 362 | cloud.speed = speed 363 | cloud.growth = growth 364 | cloud.opacity = opacity 365 | cloud.fade = fade 366 | cloud.bounds.copy(bounds) 367 | 368 | cloud.density = Math.max(0.5, random()) 369 | 370 | cloud.rotationFactor = Math.max(0.2, 0.5 * random()) * speed 371 | 372 | // Only distribute randomly if there are multiple segments 373 | 374 | const distributed = distribute?.(cloud, index) 375 | 376 | if (distributed || segments > 1) { 377 | cloud.position.copy(cloud.bounds).multiply( 378 | distributed?.point ?? 379 | ({ 380 | x: random() * 2 - 1, 381 | y: random() * 2 - 1, 382 | z: random() * 2 - 1, 383 | } as Vector3) 384 | ) 385 | } 386 | const xDiff = Math.abs(cloud.position.x) 387 | const yDiff = Math.abs(cloud.position.y) 388 | const zDiff = Math.abs(cloud.position.z) 389 | const max = Math.max(xDiff, yDiff, zDiff) 390 | cloud.length = 1 391 | if (xDiff === max) cloud.length -= xDiff / cloud.bounds.x 392 | if (yDiff === max) cloud.length -= yDiff / cloud.bounds.y 393 | if (zDiff === max) cloud.length -= zDiff / cloud.bounds.z 394 | cloud.volume = 395 | (distributed?.volume !== undefined 396 | ? distributed.volume 397 | : Math.max( 398 | Math.max(0, smallestVolume), 399 | concentrate === 'random' ? random() : concentrate === 'inside' ? cloud.length : 1 - cloud.length 400 | )) * volume 401 | }) 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/core/Grid.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { shaderMaterial } from './shaderMaterial' 3 | /** Based on 4 | https://github.com/Fyrestar/THREE.InfiniteGridHelper by https://github.com/Fyrestar 5 | and https://github.com/threlte/threlte/blob/main/packages/extras/src/lib/components/Grid/Grid.svelte 6 | by https://github.com/grischaerbe and https://github.com/jerzakm 7 | */ 8 | 9 | export type GridProps = { 10 | /** plane-geometry size, default: [1,1] */ 11 | args?: Array 12 | /** Cell size, default: 0.5 */ 13 | cellSize?: number 14 | /** Cell thickness, default: 0.5 */ 15 | cellThickness?: number 16 | /** Cell color, default: black */ 17 | cellColor?: THREE.Color 18 | /** Section size, default: 1 */ 19 | sectionSize?: number 20 | /** Section thickness, default: 1 */ 21 | sectionThickness?: number 22 | /** Section color, default: #2080ff */ 23 | sectionColor?: THREE.Color 24 | /** Follow camera, default: false */ 25 | followCamera?: boolean 26 | /** Display the grid infinitely, default: false */ 27 | infiniteGrid?: boolean 28 | /** Fade distance, default: 100 */ 29 | fadeDistance?: number 30 | /** Fade strength, default: 1 */ 31 | fadeStrength?: number 32 | /** Material side, default: THREE.BackSide */ 33 | side?: THREE.Side 34 | } 35 | 36 | const GridMaterial = shaderMaterial( 37 | { 38 | cellSize: 0.5, 39 | sectionSize: 1, 40 | fadeDistance: 100, 41 | fadeStrength: 1, 42 | cellThickness: 0.5, 43 | sectionThickness: 1, 44 | cellColor: new THREE.Color(), 45 | sectionColor: new THREE.Color(), 46 | infiniteGrid: false, 47 | followCamera: false, 48 | worldCamProjPosition: new THREE.Vector3(), 49 | worldPlanePosition: new THREE.Vector3(), 50 | }, 51 | /* glsl */ ` 52 | varying vec3 localPosition; 53 | varying vec4 worldPosition; 54 | 55 | uniform vec3 worldCamProjPosition; 56 | uniform vec3 worldPlanePosition; 57 | uniform float fadeDistance; 58 | uniform bool infiniteGrid; 59 | uniform bool followCamera; 60 | 61 | void main() { 62 | localPosition = position.xzy; 63 | if (infiniteGrid) localPosition *= 1.0 + fadeDistance; 64 | 65 | worldPosition = modelMatrix * vec4(localPosition, 1.0); 66 | if (followCamera) { 67 | worldPosition.xyz += (worldCamProjPosition - worldPlanePosition); 68 | localPosition = (inverse(modelMatrix) * worldPosition).xyz; 69 | } 70 | 71 | gl_Position = projectionMatrix * viewMatrix * worldPosition; 72 | } 73 | `, 74 | /* glsl */ ` 75 | varying vec3 localPosition; 76 | varying vec4 worldPosition; 77 | 78 | uniform vec3 worldCamProjPosition; 79 | uniform float cellSize; 80 | uniform float sectionSize; 81 | uniform vec3 cellColor; 82 | uniform vec3 sectionColor; 83 | uniform float fadeDistance; 84 | uniform float fadeStrength; 85 | uniform float cellThickness; 86 | uniform float sectionThickness; 87 | 88 | float getGrid(float size, float thickness) { 89 | vec2 r = localPosition.xz / size; 90 | vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r); 91 | float line = min(grid.x, grid.y) + 1.0 - thickness; 92 | return 1.0 - min(line, 1.0); 93 | } 94 | 95 | void main() { 96 | float g1 = getGrid(cellSize, cellThickness); 97 | float g2 = getGrid(sectionSize, sectionThickness); 98 | 99 | float dist = distance(worldCamProjPosition, worldPosition.xyz); 100 | float d = 1.0 - min(dist / fadeDistance, 1.0); 101 | vec3 color = mix(cellColor, sectionColor, min(1.0, sectionThickness * g2)); 102 | 103 | gl_FragColor = vec4(color, (g1 + g2) * pow(d, fadeStrength)); 104 | gl_FragColor.a = mix(0.75 * gl_FragColor.a, gl_FragColor.a, g2); 105 | if (gl_FragColor.a <= 0.0) discard; 106 | 107 | #include 108 | #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> 109 | } 110 | ` 111 | ) 112 | 113 | export type GridType = { 114 | /* Mesh with gridMaterial to add to your scene */ 115 | mesh: THREE.Mesh 116 | /* Call in animate loop to update grid w.r.t camera */ 117 | update: (camera: THREE.Camera) => void 118 | } 119 | 120 | export const Grid = ({ 121 | args = [1, 1], 122 | cellColor = new THREE.Color('#000000'), 123 | sectionColor = new THREE.Color('#2080ff'), 124 | cellSize = 0.5, 125 | sectionSize = 1, 126 | followCamera = false, 127 | infiniteGrid = false, 128 | fadeDistance = 100, 129 | fadeStrength = 1, 130 | cellThickness = 0.5, 131 | sectionThickness = 1, 132 | side = THREE.BackSide, 133 | }: GridProps = {}): GridType => { 134 | const uniforms1 = { cellSize, sectionSize, cellColor, sectionColor, cellThickness, sectionThickness } 135 | const uniforms2 = { fadeDistance, fadeStrength, infiniteGrid, followCamera } 136 | const gridMaterial = new GridMaterial({ 137 | transparent: true, 138 | side, 139 | ...uniforms1, 140 | ...uniforms2, 141 | }) 142 | const planeGeometry = new THREE.PlaneGeometry(args[0], args[1]) 143 | const mesh = new THREE.Mesh(planeGeometry, gridMaterial) 144 | mesh.frustumCulled = false 145 | 146 | const plane = new THREE.Plane() 147 | const upVector = new THREE.Vector3(0, 1, 0) 148 | const zeroVector = new THREE.Vector3(0, 0, 0) 149 | 150 | const update = (camera: THREE.Camera) => { 151 | if (!mesh.parent) return 152 | plane.setFromNormalAndCoplanarPoint(upVector, zeroVector).applyMatrix4(mesh.matrixWorld) 153 | const gridMaterial = mesh.material as THREE.ShaderMaterial 154 | const worldCamProjPosition = gridMaterial.uniforms.worldCamProjPosition as THREE.Uniform 155 | const worldPlanePosition = gridMaterial.uniforms.worldPlanePosition as THREE.Uniform 156 | plane.projectPoint(camera.position, worldCamProjPosition.value) 157 | worldPlanePosition.value.set(0, 0, 0).applyMatrix4(mesh.matrixWorld) 158 | } 159 | 160 | return { 161 | mesh, 162 | update, 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/core/MeshPortalMaterial.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { shaderMaterial } from './shaderMaterial' 3 | import { version } from '../helpers/constants' 4 | import { FullScreenQuad } from '../helpers/Pass' 5 | 6 | export type PortalMaterialType = { 7 | resolution: THREE.Vector2 8 | blur: number 9 | size?: number 10 | sdf?: THREE.Texture | null 11 | map?: THREE.Texture | null 12 | } 13 | 14 | export const MeshPortalMaterial = shaderMaterial( 15 | { 16 | blur: 0, 17 | map: null, 18 | sdf: null, 19 | size: 0, 20 | resolution: /* @__PURE__ */ new THREE.Vector2(), 21 | }, 22 | `varying vec2 vUv; 23 | void main() { 24 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 25 | vUv = uv; 26 | }`, 27 | `uniform sampler2D sdf; 28 | uniform sampler2D map; 29 | uniform float blur; 30 | uniform float size; 31 | uniform float time; 32 | uniform vec2 resolution; 33 | varying vec2 vUv; 34 | #include 35 | void main() { 36 | vec2 uv = gl_FragCoord.xy / resolution.xy; 37 | vec4 t = texture2D(map, uv); 38 | float k = blur; 39 | float d = texture2D(sdf, vUv).r/size; 40 | float alpha = 1.0 - smoothstep(0.0, 1.0, clamp(d/k + 1.0, 0.0, 1.0)); 41 | gl_FragColor = vec4(t.rgb, blur == 0.0 ? t.a : t.a * alpha); 42 | #include 43 | #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> 44 | }` 45 | ) 46 | 47 | const makeSDFGenerator = (clientWidth: number, clientHeight: number, renderer: THREE.WebGLRenderer) => { 48 | const finalTarget = new THREE.WebGLRenderTarget(clientWidth, clientHeight, { 49 | minFilter: THREE.LinearMipmapLinearFilter, 50 | magFilter: THREE.LinearFilter, 51 | type: THREE.FloatType, 52 | format: THREE.RedFormat, 53 | generateMipmaps: true, 54 | }) 55 | const outsideRenderTarget = new THREE.WebGLRenderTarget(clientWidth, clientHeight, { 56 | minFilter: THREE.NearestFilter, 57 | magFilter: THREE.NearestFilter, 58 | }) 59 | const insideRenderTarget = new THREE.WebGLRenderTarget(clientWidth, clientHeight, { 60 | minFilter: THREE.NearestFilter, 61 | magFilter: THREE.NearestFilter, 62 | }) 63 | const outsideRenderTarget2 = new THREE.WebGLRenderTarget(clientWidth, clientHeight, { 64 | minFilter: THREE.NearestFilter, 65 | magFilter: THREE.NearestFilter, 66 | }) 67 | const insideRenderTarget2 = new THREE.WebGLRenderTarget(clientWidth, clientHeight, { 68 | minFilter: THREE.NearestFilter, 69 | magFilter: THREE.NearestFilter, 70 | }) 71 | const outsideRenderTargetFinal = new THREE.WebGLRenderTarget(clientWidth, clientHeight, { 72 | minFilter: THREE.NearestFilter, 73 | magFilter: THREE.NearestFilter, 74 | type: THREE.FloatType, 75 | format: THREE.RedFormat, 76 | }) 77 | const insideRenderTargetFinal = new THREE.WebGLRenderTarget(clientWidth, clientHeight, { 78 | minFilter: THREE.NearestFilter, 79 | magFilter: THREE.NearestFilter, 80 | type: THREE.FloatType, 81 | format: THREE.RedFormat, 82 | }) 83 | const uvRender = new FullScreenQuad( 84 | new THREE.ShaderMaterial({ 85 | uniforms: { tex: { value: null } }, 86 | vertexShader: /*glsl*/ ` 87 | varying vec2 vUv; 88 | void main() { 89 | vUv = uv; 90 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 91 | }`, 92 | fragmentShader: /*glsl*/ ` 93 | uniform sampler2D tex; 94 | varying vec2 vUv; 95 | #include 96 | void main() { 97 | gl_FragColor = pack2HalfToRGBA(vUv * (round(texture2D(tex, vUv).x))); 98 | }`, 99 | }) 100 | ) 101 | const uvRenderInside = new FullScreenQuad( 102 | new THREE.ShaderMaterial({ 103 | uniforms: { tex: { value: null } }, 104 | vertexShader: /*glsl*/ ` 105 | varying vec2 vUv; 106 | void main() { 107 | vUv = uv; 108 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 109 | }`, 110 | fragmentShader: /*glsl*/ ` 111 | uniform sampler2D tex; 112 | varying vec2 vUv; 113 | #include 114 | void main() { 115 | gl_FragColor = pack2HalfToRGBA(vUv * (1.0 - round(texture2D(tex, vUv).x))); 116 | }`, 117 | }) 118 | ) 119 | const jumpFloodRender = new FullScreenQuad( 120 | new THREE.ShaderMaterial({ 121 | uniforms: { 122 | tex: { value: null }, 123 | offset: { value: 0.0 }, 124 | level: { value: 0.0 }, 125 | maxSteps: { value: 0.0 }, 126 | }, 127 | vertexShader: /*glsl*/ ` 128 | varying vec2 vUv; 129 | void main() { 130 | vUv = uv; 131 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 132 | }`, 133 | fragmentShader: /*glsl*/ ` 134 | varying vec2 vUv; 135 | uniform sampler2D tex; 136 | uniform float offset; 137 | uniform float level; 138 | uniform float maxSteps; 139 | #include 140 | void main() { 141 | float closestDist = 9999999.9; 142 | vec2 closestPos = vec2(0.0); 143 | for (float x = -1.0; x <= 1.0; x += 1.0) { 144 | for (float y = -1.0; y <= 1.0; y += 1.0) { 145 | vec2 voffset = vUv; 146 | voffset += vec2(x, y) * vec2(${1 / clientWidth}, ${1 / clientHeight}) * offset; 147 | vec2 pos = unpackRGBATo2Half(texture2D(tex, voffset)); 148 | float dist = distance(pos.xy, vUv); 149 | if(pos.x != 0.0 && pos.y != 0.0 && dist < closestDist) { 150 | closestDist = dist; 151 | closestPos = pos; 152 | } 153 | } 154 | } 155 | gl_FragColor = pack2HalfToRGBA(closestPos); 156 | }`, 157 | }) 158 | ) 159 | const distanceFieldRender = new FullScreenQuad( 160 | new THREE.ShaderMaterial({ 161 | uniforms: { 162 | tex: { value: null }, 163 | size: { value: new THREE.Vector2(clientWidth, clientHeight) }, 164 | }, 165 | vertexShader: /*glsl*/ ` 166 | varying vec2 vUv; 167 | void main() { 168 | vUv = uv; 169 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 170 | }`, 171 | fragmentShader: /*glsl*/ ` 172 | varying vec2 vUv; 173 | uniform sampler2D tex; 174 | uniform vec2 size; 175 | #include 176 | void main() { 177 | gl_FragColor = vec4(distance(size * unpackRGBATo2Half(texture2D(tex, vUv)), size * vUv), 0.0, 0.0, 0.0); 178 | }`, 179 | }) 180 | ) 181 | const compositeRender = new FullScreenQuad( 182 | new THREE.ShaderMaterial({ 183 | uniforms: { 184 | inside: { value: insideRenderTargetFinal.texture }, 185 | outside: { value: outsideRenderTargetFinal.texture }, 186 | tex: { value: null }, 187 | }, 188 | vertexShader: /*glsl*/ ` 189 | varying vec2 vUv; 190 | void main() { 191 | vUv = uv; 192 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 193 | }`, 194 | fragmentShader: /*glsl*/ ` 195 | varying vec2 vUv; 196 | uniform sampler2D inside; 197 | uniform sampler2D outside; 198 | uniform sampler2D tex; 199 | #include 200 | void main() { 201 | float i = texture2D(inside, vUv).x; 202 | float o =texture2D(outside, vUv).x; 203 | if (texture2D(tex, vUv).x == 0.0) { 204 | gl_FragColor = vec4(o, 0.0, 0.0, 0.0); 205 | } else { 206 | gl_FragColor = vec4(-i, 0.0, 0.0, 0.0); 207 | } 208 | }`, 209 | }) 210 | ) 211 | 212 | return (image: THREE.Texture) => { 213 | const ft = finalTarget 214 | image.minFilter = THREE.NearestFilter 215 | image.magFilter = THREE.NearestFilter 216 | uvRender.material.uniforms.tex.value = image 217 | renderer.setRenderTarget(outsideRenderTarget) 218 | uvRender.render(renderer) 219 | 220 | const passes = Math.ceil(Math.log(Math.max(clientWidth, clientHeight)) / Math.log(2.0)) 221 | let lastTarget = outsideRenderTarget 222 | let target: THREE.WebGLRenderTarget = null! 223 | for (let i = 0; i < passes; i++) { 224 | const offset = Math.pow(2, passes - i - 1) 225 | target = lastTarget === outsideRenderTarget ? outsideRenderTarget2 : outsideRenderTarget 226 | jumpFloodRender.material.uniforms.level.value = i 227 | jumpFloodRender.material.uniforms.maxSteps.value = passes 228 | jumpFloodRender.material.uniforms.offset.value = offset 229 | jumpFloodRender.material.uniforms.tex.value = lastTarget.texture 230 | renderer.setRenderTarget(target) 231 | jumpFloodRender.render(renderer) 232 | lastTarget = target 233 | } 234 | renderer.setRenderTarget(outsideRenderTargetFinal) 235 | distanceFieldRender.material.uniforms.tex.value = target.texture 236 | distanceFieldRender.render(renderer) 237 | uvRenderInside.material.uniforms.tex.value = image 238 | renderer.setRenderTarget(insideRenderTarget) 239 | uvRenderInside.render(renderer) 240 | lastTarget = insideRenderTarget 241 | 242 | for (let i = 0; i < passes; i++) { 243 | const offset = Math.pow(2, passes - i - 1) 244 | target = lastTarget === insideRenderTarget ? insideRenderTarget2 : insideRenderTarget 245 | jumpFloodRender.material.uniforms.level.value = i 246 | jumpFloodRender.material.uniforms.maxSteps.value = passes 247 | jumpFloodRender.material.uniforms.offset.value = offset 248 | jumpFloodRender.material.uniforms.tex.value = lastTarget.texture 249 | renderer.setRenderTarget(target) 250 | jumpFloodRender.render(renderer) 251 | lastTarget = target 252 | } 253 | renderer.setRenderTarget(insideRenderTargetFinal) 254 | distanceFieldRender.material.uniforms.tex.value = target.texture 255 | distanceFieldRender.render(renderer) 256 | renderer.setRenderTarget(ft) 257 | compositeRender.material.uniforms.tex.value = image 258 | compositeRender.render(renderer) 259 | renderer.setRenderTarget(null) 260 | return ft 261 | } 262 | } 263 | 264 | /** 265 | * Generate and apply sdf mask on the portalMesh 266 | * @param portalMesh mesh which uses MeshPortalMaterial 267 | * @param resolution resolution of sdf texture 268 | * @param gl renderer to help generate sdf image 269 | */ 270 | export const meshPortalMaterialApplySDF = (portalMesh: THREE.Mesh, resolution: number, gl: THREE.WebGLRenderer) => { 271 | const maskRenderTarget = new THREE.WebGLRenderTarget(resolution, resolution) 272 | const portalMat = portalMesh.material as InstanceType 273 | // Apply the SDF mask only once 274 | const tempMesh = new THREE.Mesh(portalMesh.geometry, new THREE.MeshBasicMaterial()) 275 | const boundingBox = new THREE.Box3().setFromBufferAttribute( 276 | tempMesh.geometry.attributes.position as THREE.BufferAttribute 277 | ) 278 | const orthoCam = new THREE.OrthographicCamera( 279 | boundingBox.min.x * (1 + 2 / resolution), 280 | boundingBox.max.x * (1 + 2 / resolution), 281 | boundingBox.max.y * (1 + 2 / resolution), 282 | boundingBox.min.y * (1 + 2 / resolution), 283 | 0.1, 284 | 1000 285 | ) 286 | orthoCam.position.set(0, 0, 1) 287 | orthoCam.lookAt(0, 0, 0) 288 | 289 | gl.setRenderTarget(maskRenderTarget) 290 | gl.render(tempMesh, orthoCam) 291 | const sg = makeSDFGenerator(resolution, resolution, gl) 292 | const sdf = sg(maskRenderTarget.texture) 293 | const readSdf = new Float32Array(resolution * resolution) 294 | gl.readRenderTargetPixels(sdf, 0, 0, resolution, resolution, readSdf) 295 | // Get smallest value in sdf 296 | let min = Infinity 297 | for (let i = 0; i < readSdf.length; i++) { 298 | if (readSdf[i] < min) min = readSdf[i] 299 | } 300 | min = -min 301 | 302 | portalMat.size = min 303 | portalMat.sdf = sdf.texture 304 | 305 | gl.setRenderTarget(null) 306 | } 307 | -------------------------------------------------------------------------------- /src/core/Outlines.ts: -------------------------------------------------------------------------------- 1 | import { shaderMaterial } from './shaderMaterial' 2 | import * as THREE from 'three' 3 | import { toCreasedNormals } from 'three/examples/jsm/utils/BufferGeometryUtils.js' 4 | 5 | export type OutlinesProps = { 6 | /** Outline color, default: black */ 7 | color?: THREE.Color 8 | /** Line thickness is independent of zoom, default: false */ 9 | screenspace?: boolean 10 | /** Outline opacity, default: 1 */ 11 | opacity?: number 12 | /** Outline transparency, default: false */ 13 | transparent?: boolean 14 | /** Outline thickness, default 0.05 */ 15 | thickness?: number 16 | /** Geometry crease angle (0 === no crease), default: Math.PI */ 17 | angle?: number 18 | toneMapped?: boolean 19 | polygonOffset?: boolean 20 | polygonOffsetFactor?: number 21 | renderOrder?: number 22 | /** needed if `screenspace` is true */ 23 | gl?: THREE.WebGLRenderer 24 | } 25 | 26 | export type OutlinesType = { 27 | group: THREE.Group 28 | updateProps: (props: Partial) => void 29 | /** 30 | * **Note**: Call this method to generate the outline mesh 31 | */ 32 | generate: () => void 33 | } 34 | 35 | const OutlinesMaterial = shaderMaterial( 36 | { 37 | screenspace: false, 38 | color: new THREE.Color('black'), 39 | opacity: 1, 40 | thickness: 0.05, 41 | size: new THREE.Vector2(), 42 | }, 43 | /* glsl */ ` 44 | #include 45 | #include 46 | #include 47 | uniform float thickness; 48 | uniform float screenspace; 49 | uniform vec2 size; 50 | void main() { 51 | #if defined (USE_SKINNING) 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #endif 58 | #include 59 | #include 60 | #include 61 | #include 62 | vec4 tNormal = vec4(normal, 0.0); 63 | vec4 tPosition = vec4(transformed, 1.0); 64 | #ifdef USE_INSTANCING 65 | tNormal = instanceMatrix * tNormal; 66 | tPosition = instanceMatrix * tPosition; 67 | #endif 68 | if (screenspace == 0.0) { 69 | vec3 newPosition = tPosition.xyz + tNormal.xyz * thickness; 70 | gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); 71 | } else { 72 | vec4 clipPosition = projectionMatrix * modelViewMatrix * tPosition; 73 | vec4 clipNormal = projectionMatrix * modelViewMatrix * tNormal; 74 | vec2 offset = normalize(clipNormal.xy) * thickness / size * clipPosition.w * 2.0; 75 | clipPosition.xy += offset; 76 | gl_Position = clipPosition; 77 | } 78 | }`, 79 | /* glsl */ ` 80 | uniform vec3 color; 81 | uniform float opacity; 82 | void main(){ 83 | gl_FragColor = vec4(color, opacity); 84 | #include 85 | #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> 86 | }` 87 | ) 88 | 89 | export function Outlines({ 90 | color = new THREE.Color('black'), 91 | opacity = 1, 92 | transparent = false, 93 | screenspace = false, 94 | toneMapped = true, 95 | polygonOffset = false, 96 | polygonOffsetFactor = 0, 97 | renderOrder = 0, 98 | thickness = 0.05, 99 | angle = Math.PI, 100 | gl, 101 | }: Partial = {}): OutlinesType { 102 | const group = new THREE.Group() 103 | 104 | let shapeProps: OutlinesProps = { 105 | color, 106 | opacity, 107 | transparent, 108 | screenspace, 109 | toneMapped, 110 | polygonOffset, 111 | polygonOffsetFactor, 112 | renderOrder, 113 | thickness, 114 | angle, 115 | } 116 | 117 | function updateMesh(angle?: number) { 118 | const parent = group.parent as THREE.Mesh & THREE.SkinnedMesh & THREE.InstancedMesh 119 | group.clear() 120 | if (parent && parent.geometry) { 121 | let mesh 122 | const material = new OutlinesMaterial({ side: THREE.BackSide }) 123 | if (parent.skeleton) { 124 | mesh = new THREE.SkinnedMesh() 125 | mesh.material = material 126 | mesh.bind(parent.skeleton, parent.bindMatrix) 127 | group.add(mesh) 128 | } else if (parent.isInstancedMesh) { 129 | mesh = new THREE.InstancedMesh(parent.geometry, material, parent.count) 130 | mesh.instanceMatrix = parent.instanceMatrix 131 | group.add(mesh) 132 | } else { 133 | mesh = new THREE.Mesh() 134 | mesh.material = material 135 | group.add(mesh) 136 | } 137 | mesh.geometry = angle ? toCreasedNormals(parent.geometry, angle) : parent.geometry 138 | } 139 | } 140 | 141 | function updateProps(newProps?: Partial) { 142 | shapeProps = { ...shapeProps, ...newProps } 143 | const mesh = group.children[0] as THREE.Mesh 144 | if (mesh) { 145 | const { 146 | transparent, 147 | thickness, 148 | color, 149 | opacity, 150 | screenspace, 151 | toneMapped, 152 | polygonOffset, 153 | polygonOffsetFactor, 154 | renderOrder, 155 | } = shapeProps 156 | const contextSize = new THREE.Vector2() 157 | if (!gl && shapeProps.screenspace) { 158 | console.warn('Outlines: "screenspace" requires a WebGLRenderer instance to calculate the outline size') 159 | } 160 | if (gl) gl.getSize(contextSize) 161 | 162 | Object.assign(mesh.material, { 163 | transparent, 164 | thickness, 165 | color, 166 | opacity, 167 | size: contextSize, 168 | screenspace, 169 | toneMapped, 170 | polygonOffset, 171 | polygonOffsetFactor, 172 | }) 173 | if (renderOrder !== undefined) mesh.renderOrder = renderOrder 174 | } 175 | } 176 | 177 | return { 178 | group, 179 | updateProps(props: Partial) { 180 | const angle = props.angle ?? shapeProps.angle 181 | if (angle !== shapeProps.angle) { 182 | updateMesh(angle) 183 | } 184 | updateProps(props) 185 | }, 186 | generate() { 187 | updateMesh(shapeProps.angle) 188 | updateProps(shapeProps) 189 | }, 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/core/SpriteAnimator.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | export type SpriteAnimatorProps = { 4 | startFrame?: number 5 | endFrame?: number 6 | fps?: number 7 | frameName?: string 8 | textureDataURL?: string 9 | textureImageURL: string 10 | loop?: boolean 11 | numberOfFrames?: number 12 | autoPlay?: boolean 13 | animationNames?: Array 14 | onStart?: Function 15 | onEnd?: Function 16 | onLoopEnd?: Function 17 | onFrame?: Function 18 | play?: boolean 19 | pause?: boolean 20 | flipX?: boolean 21 | position?: Array 22 | alphaTest?: number 23 | asSprite?: boolean 24 | } 25 | 26 | export type SpriteAnimatorType = { 27 | group: THREE.Group // A reference to the THREE.Group used for animations. 28 | init: Function // Function to initialize the sprite animator. 29 | update: Function // Function to update the sprite animation. 30 | pauseAnimation: Function // Function to pause the animation. 31 | playAnimation: Function // Function to play the animation. 32 | setFrameName: Function // Function to set the frame name. 33 | } 34 | 35 | export const SpriteAnimator = ({ 36 | startFrame, 37 | endFrame, 38 | fps, 39 | frameName, 40 | textureDataURL, 41 | textureImageURL, 42 | loop, 43 | numberOfFrames, 44 | autoPlay, 45 | animationNames, 46 | onStart, 47 | onEnd, 48 | onLoopEnd, 49 | onFrame, 50 | play, 51 | pause, 52 | flipX, 53 | alphaTest, 54 | asSprite, 55 | }: SpriteAnimatorProps): SpriteAnimatorType => { 56 | let spriteData = { 57 | frames: [], 58 | meta: { 59 | version: '1.0', 60 | size: { w: 1, h: 1 }, 61 | scale: '1', 62 | }, 63 | } 64 | 65 | // let isJsonReady = false 66 | let hasEnded = false 67 | 68 | let spriteTexture = new THREE.Texture() 69 | const spriteMaterial = new THREE.SpriteMaterial({ 70 | toneMapped: false, 71 | transparent: true, 72 | map: spriteTexture, 73 | alphaTest: alphaTest ?? 0.0, 74 | }) 75 | const basicMaterial = new THREE.MeshBasicMaterial({ 76 | toneMapped: false, 77 | side: THREE.DoubleSide, 78 | map: spriteTexture, 79 | transparent: true, 80 | alphaTest: alphaTest ?? 0.0, 81 | }) 82 | 83 | const spriteMesh = new THREE.Sprite(spriteMaterial) 84 | const planeMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), basicMaterial) 85 | 86 | let matRef: THREE.SpriteMaterial | THREE.MeshBasicMaterial = spriteMaterial 87 | 88 | let spriteRef: THREE.Sprite | THREE.Mesh = spriteMesh 89 | 90 | const group = new THREE.Group() 91 | group.add(spriteRef) 92 | 93 | let timerOffset = window.performance.now() 94 | // let textureData: THREE.Texture 95 | let currentFrame = startFrame || 0 96 | let currentFrameName = frameName || '' 97 | const fpsInterval = 1000 / (fps || 30) 98 | 99 | const setSpriteTexture = (texture: THREE.Texture) => { 100 | spriteTexture = texture 101 | if (matRef) { 102 | matRef.map = texture 103 | } 104 | } 105 | 106 | // let totalFrames = 0 107 | const aspect = new THREE.Vector3(1, 1, 1) 108 | const setAspect = (v: THREE.Vector3) => { 109 | aspect.copy(v) 110 | } 111 | 112 | const flipOffset = flipX ? -1 : 1 113 | 114 | let displayAsSprite = asSprite ?? true 115 | const setDisplayAsSprite = (state: boolean) => { 116 | displayAsSprite = state 117 | if (displayAsSprite) { 118 | matRef = spriteMaterial 119 | spriteRef = spriteMesh 120 | group.add(spriteMesh) 121 | group.remove(planeMesh) 122 | } else { 123 | matRef = basicMaterial 124 | spriteRef = planeMesh 125 | group.remove(spriteMesh) 126 | group.add(planeMesh) 127 | } 128 | } 129 | 130 | setDisplayAsSprite(displayAsSprite) 131 | 132 | async function loadJsonAndTextureAndExecuteCallback( 133 | jsonUrl: string, 134 | textureUrl: string, 135 | callback: (json: any, texture: THREE.Texture) => void 136 | ) { 137 | const textureLoader = new THREE.TextureLoader() 138 | const jsonPromise = fetch(jsonUrl).then((response) => response.json()) 139 | const texturePromise = new Promise((resolve) => { 140 | textureLoader.load(textureUrl, resolve) 141 | }) 142 | 143 | await Promise.all([jsonPromise, texturePromise]).then((response) => { 144 | callback(response[0], response[1]) 145 | }) 146 | } 147 | 148 | const calculateAspectRatio = (width: number, height: number): THREE.Vector3 => { 149 | const aspectRatio = height / width 150 | spriteRef.scale.set(1, aspectRatio, 1) 151 | return spriteRef.scale 152 | } 153 | 154 | // initial loads 155 | const init = async () => { 156 | if (textureDataURL && textureImageURL) { 157 | await loadJsonAndTextureAndExecuteCallback(textureDataURL, textureImageURL, parseSpriteData) 158 | } else if (textureImageURL) { 159 | // only load the texture, this is an image sprite only 160 | const textureLoader = new THREE.TextureLoader() 161 | const texture = await textureLoader.loadAsync(textureImageURL) 162 | parseSpriteData(null, texture) 163 | } 164 | } 165 | 166 | const setFrameName = (name: string) => { 167 | frameName = name 168 | if (currentFrameName !== frameName && frameName) { 169 | currentFrame = 0 170 | currentFrameName = frameName 171 | } 172 | } 173 | 174 | const pauseAnimation = () => { 175 | pause = true 176 | } 177 | 178 | const playAnimation = () => { 179 | play = true 180 | pause = false 181 | } 182 | 183 | const parseSpriteData = (json: any, _spriteTexture: THREE.Texture): void => { 184 | // sprite only case 185 | if (json === null) { 186 | if (_spriteTexture && numberOfFrames) { 187 | //get size from texture 188 | const width = _spriteTexture.image.width 189 | const height = _spriteTexture.image.height 190 | const frameWidth = width / numberOfFrames 191 | const frameHeight = height 192 | // textureData = _spriteTexture 193 | // totalFrames = numberOfFrames 194 | spriteData = { 195 | frames: [], 196 | meta: { 197 | version: '1.0', 198 | size: { w: width, h: height }, 199 | scale: '1', 200 | }, 201 | } 202 | 203 | if (parseInt(frameWidth.toString(), 10) === frameWidth) { 204 | // if it fits 205 | for (let i = 0; i < numberOfFrames; i++) { 206 | spriteData.frames.push({ 207 | frame: { x: i * frameWidth, y: 0, w: frameWidth, h: frameHeight }, 208 | rotated: false, 209 | trimmed: false, 210 | spriteSourceSize: { x: 0, y: 0, w: frameWidth, h: frameHeight }, 211 | sourceSize: { w: frameWidth, h: height }, 212 | }) 213 | } 214 | } 215 | } 216 | } else if (_spriteTexture) { 217 | spriteData = json 218 | spriteData.frames = Array.isArray(json.frames) ? json.frames : parseFrames() 219 | // totalFrames = Array.isArray(json.frames) ? json.frames.length : Object.keys(json.frames).length 220 | // textureData = _spriteTexture 221 | 222 | const { w, h } = getFirstItem(json.frames).sourceSize 223 | const aspect = calculateAspectRatio(w, h) 224 | 225 | setAspect(aspect) 226 | if (matRef) { 227 | matRef.map = _spriteTexture 228 | } 229 | } 230 | 231 | _spriteTexture.premultiplyAlpha = false 232 | 233 | setSpriteTexture(_spriteTexture) 234 | modifySpritePosition() 235 | } 236 | 237 | // for frame based JSON Hash sprite data 238 | const parseFrames = (): any => { 239 | const sprites: any = {} 240 | const data = spriteData 241 | const delimiters = animationNames 242 | if (delimiters) { 243 | for (let i = 0; i < delimiters.length; i++) { 244 | sprites[delimiters[i]] = [] 245 | 246 | for (const innerKey in data['frames']) { 247 | const value = data['frames'][innerKey] 248 | const frameData = value['frame'] 249 | const x = frameData['x'] 250 | const y = frameData['y'] 251 | const width = frameData['w'] 252 | const height = frameData['h'] 253 | const sourceWidth = value['sourceSize']['w'] 254 | const sourceHeight = value['sourceSize']['h'] 255 | 256 | if (typeof innerKey === 'string' && innerKey.toLowerCase().indexOf(delimiters[i].toLowerCase()) !== -1) { 257 | sprites[delimiters[i]].push({ 258 | x: x, 259 | y: y, 260 | w: width, 261 | h: height, 262 | frame: frameData, 263 | sourceSize: { w: sourceWidth, h: sourceHeight }, 264 | }) 265 | } 266 | } 267 | } 268 | } 269 | 270 | return sprites 271 | } 272 | 273 | // modify the sprite material after json is parsed and state updated 274 | const modifySpritePosition = () => { 275 | if (!(spriteData && matRef.map)) return 276 | const { 277 | meta: { size: metaInfo }, 278 | frames, 279 | } = spriteData 280 | 281 | const { w: frameW, h: frameH } = Array.isArray(frames) 282 | ? frames[0].sourceSize 283 | : frameName 284 | ? frames[frameName] 285 | ? frames[frameName][0].sourceSize 286 | : { w: 0, h: 0 } 287 | : { w: 0, h: 0 } 288 | 289 | matRef.map.wrapS = matRef.map.wrapT = THREE.RepeatWrapping 290 | matRef.map.center.set(0, 0) 291 | matRef.map.repeat.set((1 * flipOffset) / (metaInfo.w / frameW), 1 / (metaInfo.h / frameH)) 292 | 293 | //const framesH = (metaInfo.w - 1) / frameW 294 | const framesV = (metaInfo.h - 1) / frameH 295 | const frameOffsetY = 1 / framesV 296 | matRef.map.offset.x = 0.0 //-matRef.map.repeat.x 297 | matRef.map.offset.y = 1 - frameOffsetY 298 | 299 | // isJsonReady = true 300 | if (onStart) onStart({ currentFrameName: frameName, currentFrame: currentFrame }) 301 | } 302 | 303 | // run the animation on each frame 304 | const runAnimation = (): void => { 305 | if (!(spriteData && matRef.map)) return 306 | const now = window.performance.now() 307 | const diff = now - timerOffset 308 | const { 309 | meta: { size: metaInfo }, 310 | frames, 311 | } = spriteData 312 | const { w: frameW, h: frameH } = getFirstItem(frames).sourceSize 313 | const spriteFrames = Array.isArray(frames) ? frames : frameName ? frames[frameName] : [] 314 | 315 | let finalValX = 0 316 | let finalValY = 0 317 | const _endFrame = endFrame || spriteFrames.length - 1 318 | 319 | if (currentFrame > _endFrame) { 320 | currentFrame = loop ? startFrame ?? 0 : 0 321 | if (loop) { 322 | onLoopEnd?.({ 323 | currentFrameName: frameName, 324 | currentFrame: currentFrame, 325 | }) 326 | } else { 327 | onEnd?.({ 328 | currentFrameName: frameName, 329 | currentFrame: currentFrame, 330 | }) 331 | hasEnded = true 332 | } 333 | if (!loop) return 334 | } 335 | 336 | if (diff <= fpsInterval) return 337 | timerOffset = now - (diff % fpsInterval) 338 | 339 | calculateAspectRatio(frameW, frameH) 340 | const framesH = (metaInfo.w - 1) / frameW 341 | const framesV = (metaInfo.h - 1) / frameH 342 | const { 343 | frame: { x: frameX, y: frameY }, 344 | sourceSize: { w: originalSizeX, h: originalSizeY }, 345 | } = spriteFrames[currentFrame] 346 | const frameOffsetX = 1 / framesH 347 | const frameOffsetY = 1 / framesV 348 | finalValX = 349 | flipOffset > 0 350 | ? frameOffsetX * (frameX / originalSizeX) 351 | : frameOffsetX * (frameX / originalSizeX) - matRef.map.repeat.x 352 | finalValY = Math.abs(1 - frameOffsetY) - frameOffsetY * (frameY / originalSizeY) 353 | 354 | matRef.map.offset.x = finalValX 355 | matRef.map.offset.y = finalValY 356 | 357 | currentFrame += 1 358 | } 359 | 360 | // *** Warning! It runs on every frame! *** 361 | const update = () => { 362 | if (!spriteData?.frames || !matRef?.map) { 363 | return 364 | } 365 | 366 | if (pause) { 367 | return 368 | } 369 | 370 | if (!hasEnded && (autoPlay || play)) { 371 | runAnimation() 372 | onFrame && onFrame({ currentFrameName: currentFrameName, currentFrame: currentFrame }) 373 | } 374 | } 375 | 376 | // utils 377 | const getFirstItem = (param: any): any => { 378 | if (Array.isArray(param)) { 379 | return param[0] 380 | } else if (typeof param === 'object' && param !== null) { 381 | const keys = Object.keys(param) 382 | return param[keys[0]][0] 383 | } else { 384 | return { w: 0, h: 0 } 385 | } 386 | } 387 | 388 | return { 389 | group, 390 | init, 391 | update, 392 | playAnimation, 393 | pauseAnimation, 394 | setFrameName, 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | // Shaders 2 | export * from './pcss' 3 | export * from './Caustics' 4 | export * from './shaderMaterial' 5 | 6 | // Staging/Prototyping 7 | export * from './AccumulativeShadows' 8 | export * from './Cloud' 9 | 10 | // Misc 11 | export * from './useFBO' 12 | export * from './SpriteAnimator' 13 | 14 | // Abstractions 15 | export * from './Outlines' 16 | export * from './Billboard' 17 | export * from './Splat' 18 | 19 | // Gizmos 20 | export * from './Grid' 21 | 22 | // Portals 23 | export * from './MeshPortalMaterial' 24 | -------------------------------------------------------------------------------- /src/core/pcss.ts: -------------------------------------------------------------------------------- 1 | /* Integration and compilation: @N8Programs 2 | Inspired by: 3 | https://github.com/mrdoob/three.js/blob/dev/examples/webgl_shadowmap_pcss.html 4 | https://developer.nvidia.com/gpugems/gpugems2/part-ii-shading-lighting-and-shadows/chapter-17-efficient-soft-edged-shadows-using 5 | https://developer.download.nvidia.com/whitepapers/2008/PCSS_Integration.pdf 6 | https://github.com/mrdoob/three.js/blob/master/examples/webgl_shadowmap_pcss.html [spidersharma03] 7 | https://spline.design/ 8 | Concept: 9 | https://www.gamedev.net/tutorials/programming/graphics/contact-hardening-soft-shadows-made-fast-r4906/ 10 | Vogel Disk Implementation: 11 | https://www.shadertoy.com/view/4l3yRM [ashalah] 12 | High-Frequency Noise Implementation: 13 | https://www.shadertoy.com/view/tt3fDH [spawner64] 14 | */ 15 | 16 | import * as THREE from 'three' 17 | 18 | type SoftShadowsProps = { 19 | /** Size of the light source (the larger the softer the light), default: 25 */ 20 | size?: number 21 | /** Number of samples (more samples less noise but more expensive), default: 10 */ 22 | samples?: number 23 | /** Depth focus, use it to shift the focal point (where the shadow is the sharpest), default: 0 (the beginning) */ 24 | focus?: number 25 | } 26 | 27 | function reset(gl, scene, camera) { 28 | scene.traverse((object) => { 29 | if (object.material) { 30 | if (Array.isArray(object.material)) { 31 | object.material.forEach((mat) => { 32 | gl.properties.remove(mat) 33 | mat.dispose() 34 | }) 35 | } else { 36 | gl.properties.remove(object.material) 37 | object.material.dispose() 38 | } 39 | } 40 | }) 41 | gl.info.programs.length = 0 42 | gl.compile(scene, camera) 43 | } 44 | 45 | export const pcss = ({ focus = 0, size = 25, samples = 10 }: SoftShadowsProps = {}) => { 46 | const original = THREE.ShaderChunk.shadowmap_pars_fragment 47 | THREE.ShaderChunk.shadowmap_pars_fragment = THREE.ShaderChunk.shadowmap_pars_fragment 48 | .replace( 49 | '#ifdef USE_SHADOWMAP', 50 | '#ifdef USE_SHADOWMAP\n' + 51 | ` 52 | #define PENUMBRA_FILTER_SIZE float(${size}) 53 | #define RGB_NOISE_FUNCTION(uv) (randRGB(uv)) 54 | vec3 randRGB(vec2 uv) { 55 | return vec3( 56 | fract(sin(dot(uv, vec2(12.75613, 38.12123))) * 13234.76575), 57 | fract(sin(dot(uv, vec2(19.45531, 58.46547))) * 43678.23431), 58 | fract(sin(dot(uv, vec2(23.67817, 78.23121))) * 93567.23423) 59 | ); 60 | } 61 | 62 | vec3 lowPassRandRGB(vec2 uv) { 63 | // 3x3 convolution (average) 64 | // can be implemented as separable with an extra buffer for a total of 6 samples instead of 9 65 | vec3 result = vec3(0); 66 | result += RGB_NOISE_FUNCTION(uv + vec2(-1.0, -1.0)); 67 | result += RGB_NOISE_FUNCTION(uv + vec2(-1.0, 0.0)); 68 | result += RGB_NOISE_FUNCTION(uv + vec2(-1.0, +1.0)); 69 | result += RGB_NOISE_FUNCTION(uv + vec2( 0.0, -1.0)); 70 | result += RGB_NOISE_FUNCTION(uv + vec2( 0.0, 0.0)); 71 | result += RGB_NOISE_FUNCTION(uv + vec2( 0.0, +1.0)); 72 | result += RGB_NOISE_FUNCTION(uv + vec2(+1.0, -1.0)); 73 | result += RGB_NOISE_FUNCTION(uv + vec2(+1.0, 0.0)); 74 | result += RGB_NOISE_FUNCTION(uv + vec2(+1.0, +1.0)); 75 | result *= 0.111111111; // 1.0 / 9.0 76 | return result; 77 | } 78 | vec3 highPassRandRGB(vec2 uv) { 79 | // by subtracting the low-pass signal from the original signal, we're being left with the high-pass signal 80 | // hp(x) = x - lp(x) 81 | return RGB_NOISE_FUNCTION(uv) - lowPassRandRGB(uv) + 0.5; 82 | } 83 | 84 | 85 | vec2 vogelDiskSample(int sampleIndex, int sampleCount, float angle) { 86 | const float goldenAngle = 2.399963f; // radians 87 | float r = sqrt(float(sampleIndex) + 0.5f) / sqrt(float(sampleCount)); 88 | float theta = float(sampleIndex) * goldenAngle + angle; 89 | float sine = sin(theta); 90 | float cosine = cos(theta); 91 | return vec2(cosine, sine) * r; 92 | } 93 | float penumbraSize( const in float zReceiver, const in float zBlocker ) { // Parallel plane estimation 94 | return (zReceiver - zBlocker) / zBlocker; 95 | } 96 | float findBlocker(sampler2D shadowMap, vec2 uv, float compare, float angle) { 97 | float texelSize = 1.0 / float(textureSize(shadowMap, 0).x); 98 | float blockerDepthSum = float(${focus}); 99 | float blockers = 0.0; 100 | 101 | int j = 0; 102 | vec2 offset = vec2(0.); 103 | float depth = 0.; 104 | 105 | #pragma unroll_loop_start 106 | for(int i = 0; i < ${samples}; i ++) { 107 | offset = (vogelDiskSample(j, ${samples}, angle) * texelSize) * 2.0 * PENUMBRA_FILTER_SIZE; 108 | depth = unpackRGBAToDepth( texture2D( shadowMap, uv + offset)); 109 | if (depth < compare) { 110 | blockerDepthSum += depth; 111 | blockers++; 112 | } 113 | j++; 114 | } 115 | #pragma unroll_loop_end 116 | 117 | if (blockers > 0.0) { 118 | return blockerDepthSum / blockers; 119 | } 120 | return -1.0; 121 | } 122 | 123 | float vogelFilter(sampler2D shadowMap, vec2 uv, float zReceiver, float filterRadius, float angle) { 124 | float texelSize = 1.0 / float(textureSize(shadowMap, 0).x); 125 | float shadow = 0.0f; 126 | int j = 0; 127 | vec2 vogelSample = vec2(0.0); 128 | vec2 offset = vec2(0.0); 129 | #pragma unroll_loop_start 130 | for (int i = 0; i < ${samples}; i++) { 131 | vogelSample = vogelDiskSample(j, ${samples}, angle) * texelSize; 132 | offset = vogelSample * (1.0 + filterRadius * float(${size})); 133 | shadow += step( zReceiver, unpackRGBAToDepth( texture2D( shadowMap, uv + offset ) ) ); 134 | j++; 135 | } 136 | #pragma unroll_loop_end 137 | return shadow * 1.0 / ${samples}.0; 138 | } 139 | 140 | float PCSS (sampler2D shadowMap, vec4 coords) { 141 | vec2 uv = coords.xy; 142 | float zReceiver = coords.z; // Assumed to be eye-space z in this code 143 | float angle = highPassRandRGB(gl_FragCoord.xy).r * PI2; 144 | float avgBlockerDepth = findBlocker(shadowMap, uv, zReceiver, angle); 145 | if (avgBlockerDepth == -1.0) { 146 | return 1.0; 147 | } 148 | float penumbraRatio = penumbraSize(zReceiver, avgBlockerDepth); 149 | return vogelFilter(shadowMap, uv, zReceiver, 1.25 * penumbraRatio, angle); 150 | }` 151 | ) 152 | .replace( 153 | '#if defined( SHADOWMAP_TYPE_PCF )', 154 | '\nreturn mix( 1.0, PCSS(shadowMap, shadowCoord), shadowIntensity );\n#if defined( SHADOWMAP_TYPE_PCF )' 155 | ) 156 | return (gl: THREE.Renderer, scene: THREE.Scene, camera: THREE.Camera) => { 157 | THREE.ShaderChunk.shadowmap_pars_fragment = original 158 | reset(gl, scene, camera) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/core/shaderMaterial.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | type UniformValue = 4 | | THREE.CubeTexture 5 | | THREE.Texture 6 | | Int32Array 7 | | Float32Array 8 | | THREE.Matrix4 9 | | THREE.Matrix3 10 | | THREE.Quaternion 11 | | THREE.Vector4 12 | | THREE.Vector3 13 | | THREE.Vector2 14 | | THREE.Color 15 | | number 16 | | boolean 17 | | Array 18 | | null 19 | 20 | type UniformProps = { [name: string]: UniformValue } 21 | 22 | type ShaderMaterialInstance = THREE.ShaderMaterial & TProps 23 | 24 | type ShaderMaterialParameters = THREE.ShaderMaterialParameters & Partial 25 | 26 | type ShaderMaterial = (new ( 27 | parameters?: ShaderMaterialParameters 28 | ) => ShaderMaterialInstance) & { key: string } 29 | 30 | export function shaderMaterial( 31 | uniforms: TProps, 32 | vertexShader: string, 33 | fragmentShader: string, 34 | onInit?: (material: ShaderMaterialInstance) => void 35 | ) { 36 | const entries = Object.entries(uniforms) 37 | 38 | class Material extends THREE.ShaderMaterial { 39 | static key = THREE.MathUtils.generateUUID() 40 | 41 | constructor(parameters?: ShaderMaterialParameters) { 42 | super({ 43 | uniforms: entries.reduce((acc, [name, value]) => { 44 | const uniform = THREE.UniformsUtils.clone({ [name]: { value } }) 45 | return { 46 | ...acc, 47 | ...uniform, 48 | } 49 | }, {}), 50 | vertexShader, 51 | fragmentShader, 52 | }) 53 | 54 | for (const [name] of entries) { 55 | Object.defineProperty(this, name, { 56 | get: () => this.uniforms[name].value, 57 | set: (v) => (this.uniforms[name].value = v), 58 | }) 59 | } 60 | 61 | Object.assign(this, parameters) 62 | 63 | onInit?.(this as unknown as ShaderMaterialInstance) 64 | } 65 | } 66 | 67 | return Material as ShaderMaterial 68 | } 69 | -------------------------------------------------------------------------------- /src/core/useFBO.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | type FBOSettings = { 4 | /** Defines the count of MSAA samples. Can only be used with WebGL 2. Default: 0 */ 5 | samples?: number 6 | /** If set, the scene depth will be rendered into buffer.depthTexture. Default: false */ 7 | depth?: boolean 8 | } & THREE.RenderTargetOptions 9 | 10 | function useFBO( 11 | /** Width in pixels */ 12 | width = 1024, 13 | /** Height in pixels */ 14 | height = 1024, 15 | /**Settings */ 16 | settings: FBOSettings = { 17 | samples: 0, 18 | depth: false, 19 | } 20 | ): THREE.WebGLRenderTarget { 21 | var _width = width 22 | var _height = height 23 | var _settings = settings 24 | var samples = _settings.samples || 0 25 | var depth = _settings.depth 26 | var targetSettings = Object.assign({}, _settings) 27 | delete targetSettings.samples 28 | delete targetSettings.depth 29 | var target = new THREE.WebGLRenderTarget( 30 | _width, 31 | _height, 32 | Object.assign( 33 | { 34 | minFilter: THREE.LinearFilter, 35 | magFilter: THREE.LinearFilter, 36 | type: THREE.HalfFloatType, 37 | }, 38 | targetSettings 39 | ) 40 | ) 41 | 42 | if (depth) { 43 | target.depthTexture = new THREE.DepthTexture(_width, _height, THREE.FloatType) 44 | } 45 | 46 | target.samples = samples 47 | 48 | return target 49 | } 50 | 51 | export { useFBO } 52 | -------------------------------------------------------------------------------- /src/helpers/Pass.ts: -------------------------------------------------------------------------------- 1 | import { OrthographicCamera, PlaneGeometry, Mesh, Material, Renderer, WebGLRenderer, WebGLRenderTarget } from 'three' 2 | 3 | class Pass { 4 | // if set to true, the pass is processed by the composer 5 | public enabled = true 6 | 7 | // if set to true, the pass indicates to swap read and write buffer after rendering 8 | public needsSwap = true 9 | 10 | // if set to true, the pass clears its buffer before rendering 11 | public clear = false 12 | 13 | // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer. 14 | public renderToScreen = false 15 | 16 | public setSize(width: number, height: number): void {} 17 | 18 | public render( 19 | renderer: WebGLRenderer, 20 | writeBuffer: WebGLRenderTarget, 21 | readBuffer: WebGLRenderTarget, 22 | deltaTime: number, 23 | maskActive?: unknown 24 | ): void { 25 | console.error('THREE.Pass: .render() must be implemented in derived pass.') 26 | } 27 | 28 | public dispose() {} 29 | } 30 | 31 | // Helper for passes that need to fill the viewport with a single quad. 32 | class FullScreenQuad { 33 | public camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1) 34 | public geometry = new PlaneGeometry(2, 2) 35 | private mesh: Mesh 36 | 37 | constructor(material: TMaterial) { 38 | this.mesh = new Mesh(this.geometry, material) 39 | } 40 | 41 | public get material(): TMaterial { 42 | return this.mesh.material 43 | } 44 | 45 | public set material(value: TMaterial) { 46 | this.mesh.material = value 47 | } 48 | 49 | public dispose(): void { 50 | this.mesh.geometry.dispose() 51 | } 52 | 53 | public render(renderer: Renderer): void { 54 | renderer.render(this.mesh, this.camera) 55 | } 56 | } 57 | 58 | export { Pass, FullScreenQuad } 59 | -------------------------------------------------------------------------------- /src/helpers/constants.ts: -------------------------------------------------------------------------------- 1 | import { REVISION } from 'three' 2 | 3 | const getVersion = () => parseInt(REVISION.replace(/\D+/g, '')) 4 | 5 | export const version = /* @__PURE__ */ getVersion() 6 | -------------------------------------------------------------------------------- /src/helpers/deprecated.ts: -------------------------------------------------------------------------------- 1 | import { BufferAttribute } from 'three' 2 | 3 | /** 4 | * Sets `BufferAttribute.updateRange` since r159. 5 | */ 6 | export const setUpdateRange = (attribute: BufferAttribute, updateRange: { offset: number; count: number }): void => { 7 | if ('updateRanges' in attribute) { 8 | // r159 9 | // @ts-ignore 10 | attribute.updateRanges[0] = updateRange 11 | } else { 12 | // @ts-ignore 13 | attribute.updateRange = updateRange 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/helpers/glsl/distort.vert.glsl: -------------------------------------------------------------------------------- 1 | #pragma glslify: snoise3 = require(glsl-noise/simplex/3d) 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core' 2 | export * from './materials' 3 | -------------------------------------------------------------------------------- /src/materials/BlurPass.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Mesh, 3 | BufferGeometry, 4 | BufferAttribute, 5 | LinearFilter, 6 | Scene, 7 | WebGLRenderTarget, 8 | WebGLRenderer, 9 | Camera, 10 | Vector2, 11 | HalfFloatType, 12 | } from 'three' 13 | 14 | import { ConvolutionMaterial } from './ConvolutionMaterial' 15 | 16 | export interface BlurPassProps { 17 | gl: WebGLRenderer 18 | resolution: number 19 | width?: number 20 | height?: number 21 | minDepthThreshold?: number 22 | maxDepthThreshold?: number 23 | depthScale?: number 24 | depthToBlurRatioBias?: number 25 | } 26 | 27 | export class BlurPass { 28 | readonly renderTargetA: WebGLRenderTarget 29 | readonly renderTargetB: WebGLRenderTarget 30 | readonly convolutionMaterial: ConvolutionMaterial 31 | readonly scene: Scene 32 | readonly camera: Camera 33 | readonly screen: Mesh 34 | renderToScreen: boolean = false 35 | 36 | constructor({ 37 | gl, 38 | resolution, 39 | width = 500, 40 | height = 500, 41 | minDepthThreshold = 0, 42 | maxDepthThreshold = 1, 43 | depthScale = 0, 44 | depthToBlurRatioBias = 0.25, 45 | }: BlurPassProps) { 46 | this.renderTargetA = new WebGLRenderTarget(resolution, resolution, { 47 | minFilter: LinearFilter, 48 | magFilter: LinearFilter, 49 | stencilBuffer: false, 50 | depthBuffer: false, 51 | type: HalfFloatType, 52 | }) 53 | this.renderTargetB = this.renderTargetA.clone() 54 | this.convolutionMaterial = new ConvolutionMaterial() 55 | this.convolutionMaterial.setTexelSize(1.0 / width, 1.0 / height) 56 | this.convolutionMaterial.setResolution(new Vector2(width, height)) 57 | this.scene = new Scene() 58 | this.camera = new Camera() 59 | this.convolutionMaterial.uniforms.minDepthThreshold.value = minDepthThreshold 60 | this.convolutionMaterial.uniforms.maxDepthThreshold.value = maxDepthThreshold 61 | this.convolutionMaterial.uniforms.depthScale.value = depthScale 62 | this.convolutionMaterial.uniforms.depthToBlurRatioBias.value = depthToBlurRatioBias 63 | this.convolutionMaterial.defines.USE_DEPTH = depthScale > 0 64 | const vertices = new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]) 65 | const uvs = new Float32Array([0, 0, 2, 0, 0, 2]) 66 | const geometry = new BufferGeometry() 67 | geometry.setAttribute('position', new BufferAttribute(vertices, 3)) 68 | geometry.setAttribute('uv', new BufferAttribute(uvs, 2)) 69 | this.screen = new Mesh(geometry, this.convolutionMaterial) 70 | this.screen.frustumCulled = false 71 | this.scene.add(this.screen) 72 | } 73 | 74 | render(renderer: WebGLRenderer, inputBuffer: WebGLRenderTarget, outputBuffer: WebGLRenderTarget) { 75 | const scene = this.scene 76 | const camera = this.camera 77 | const renderTargetA = this.renderTargetA 78 | const renderTargetB = this.renderTargetB 79 | const material = this.convolutionMaterial 80 | const uniforms = material.uniforms 81 | uniforms.depthBuffer.value = inputBuffer.depthTexture 82 | const kernel = material.kernel 83 | let lastRT = inputBuffer 84 | let destRT: WebGLRenderTarget 85 | let i: number, l: number 86 | // Apply the multi-pass blur. 87 | for (i = 0, l = kernel.length - 1; i < l; ++i) { 88 | // Alternate between targets. 89 | destRT = (i & 1) === 0 ? renderTargetA : renderTargetB 90 | uniforms.kernel.value = kernel[i] 91 | uniforms.inputBuffer.value = lastRT.texture 92 | renderer.setRenderTarget(destRT) 93 | renderer.render(scene, camera) 94 | lastRT = destRT 95 | } 96 | uniforms.kernel.value = kernel[i] 97 | uniforms.inputBuffer.value = lastRT.texture 98 | renderer.setRenderTarget(this.renderToScreen ? null : outputBuffer) 99 | renderer.render(scene, camera) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/materials/ConvolutionMaterial.ts: -------------------------------------------------------------------------------- 1 | import { NoBlending, ShaderMaterial, Uniform, Vector2, REVISION } from 'three' 2 | 3 | export class ConvolutionMaterial extends ShaderMaterial { 4 | readonly kernel: Float32Array 5 | constructor(texelSize = new Vector2()) { 6 | super({ 7 | uniforms: { 8 | inputBuffer: new Uniform(null), 9 | depthBuffer: new Uniform(null), 10 | resolution: new Uniform(new Vector2()), 11 | texelSize: new Uniform(new Vector2()), 12 | halfTexelSize: new Uniform(new Vector2()), 13 | kernel: new Uniform(0.0), 14 | scale: new Uniform(1.0), 15 | cameraNear: new Uniform(0.0), 16 | cameraFar: new Uniform(1.0), 17 | minDepthThreshold: new Uniform(0.0), 18 | maxDepthThreshold: new Uniform(1.0), 19 | depthScale: new Uniform(0.0), 20 | depthToBlurRatioBias: new Uniform(0.25), 21 | }, 22 | fragmentShader: `#include 23 | #include 24 | uniform sampler2D inputBuffer; 25 | uniform sampler2D depthBuffer; 26 | uniform float cameraNear; 27 | uniform float cameraFar; 28 | uniform float minDepthThreshold; 29 | uniform float maxDepthThreshold; 30 | uniform float depthScale; 31 | uniform float depthToBlurRatioBias; 32 | varying vec2 vUv; 33 | varying vec2 vUv0; 34 | varying vec2 vUv1; 35 | varying vec2 vUv2; 36 | varying vec2 vUv3; 37 | 38 | void main() { 39 | float depthFactor = 0.0; 40 | 41 | #ifdef USE_DEPTH 42 | vec4 depth = texture2D(depthBuffer, vUv); 43 | depthFactor = smoothstep(minDepthThreshold, maxDepthThreshold, 1.0-(depth.r * depth.a)); 44 | depthFactor *= depthScale; 45 | depthFactor = max(0.0, min(1.0, depthFactor + 0.25)); 46 | #endif 47 | 48 | vec4 sum = texture2D(inputBuffer, mix(vUv0, vUv, depthFactor)); 49 | sum += texture2D(inputBuffer, mix(vUv1, vUv, depthFactor)); 50 | sum += texture2D(inputBuffer, mix(vUv2, vUv, depthFactor)); 51 | sum += texture2D(inputBuffer, mix(vUv3, vUv, depthFactor)); 52 | gl_FragColor = sum * 0.25 ; 53 | 54 | #include 55 | #include 56 | #include <${parseInt(REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> 57 | }`, 58 | vertexShader: `uniform vec2 texelSize; 59 | uniform vec2 halfTexelSize; 60 | uniform float kernel; 61 | uniform float scale; 62 | varying vec2 vUv; 63 | varying vec2 vUv0; 64 | varying vec2 vUv1; 65 | varying vec2 vUv2; 66 | varying vec2 vUv3; 67 | 68 | void main() { 69 | vec2 uv = position.xy * 0.5 + 0.5; 70 | vUv = uv; 71 | 72 | vec2 dUv = (texelSize * vec2(kernel) + halfTexelSize) * scale; 73 | vUv0 = vec2(uv.x - dUv.x, uv.y + dUv.y); 74 | vUv1 = vec2(uv.x + dUv.x, uv.y + dUv.y); 75 | vUv2 = vec2(uv.x + dUv.x, uv.y - dUv.y); 76 | vUv3 = vec2(uv.x - dUv.x, uv.y - dUv.y); 77 | 78 | gl_Position = vec4(position.xy, 1.0, 1.0); 79 | }`, 80 | blending: NoBlending, 81 | depthWrite: false, 82 | depthTest: false, 83 | }) 84 | 85 | this.toneMapped = false 86 | this.setTexelSize(texelSize.x, texelSize.y) 87 | this.kernel = new Float32Array([0.0, 1.0, 2.0, 2.0, 3.0]) 88 | } 89 | 90 | setTexelSize(x: number, y: number) { 91 | this.uniforms.texelSize.value.set(x, y) 92 | this.uniforms.halfTexelSize.value.set(x, y).multiplyScalar(0.5) 93 | } 94 | setResolution(resolution: Vector2) { 95 | this.uniforms.resolution.value.copy(resolution) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/materials/MeshDiscardMaterial.ts: -------------------------------------------------------------------------------- 1 | import { shaderMaterial } from '../core/shaderMaterial' 2 | 3 | export const MeshDiscardMaterial = shaderMaterial( 4 | {}, 5 | 'void main() { }', 6 | 'void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); discard; }' 7 | ) 8 | -------------------------------------------------------------------------------- /src/materials/MeshDistortMaterial.ts: -------------------------------------------------------------------------------- 1 | import { IUniform, MeshPhysicalMaterial, MeshPhysicalMaterialParameters } from 'three' 2 | // @ts-ignore 3 | import distort from '../helpers/glsl/distort.vert.glsl' 4 | 5 | export interface MeshDistortMaterialParameters { 6 | time?: number 7 | distort?: number 8 | radius?: number 9 | } 10 | 11 | export class MeshDistortMaterial extends MeshPhysicalMaterial { 12 | _time: IUniform 13 | _distort: IUniform 14 | _radius: IUniform 15 | 16 | constructor({ 17 | time = 0, 18 | distort = 0.4, 19 | radius = 1, 20 | ...parameters 21 | }: MeshDistortMaterialParameters & MeshPhysicalMaterialParameters = {}) { 22 | super(parameters) 23 | this.setValues(parameters) 24 | this._time = { value: time } 25 | this._distort = { value: distort } 26 | this._radius = { value: radius } 27 | } 28 | 29 | // FIXME Use `THREE.WebGLProgramParametersWithUniforms` type when able to target @types/three@0.160.0 30 | onBeforeCompile(shader: { vertexShader: string; uniforms: { [uniform: string]: IUniform } }) { 31 | shader.uniforms.time = this._time 32 | shader.uniforms.radius = this._radius 33 | shader.uniforms.distort = this._distort 34 | 35 | shader.vertexShader = ` 36 | uniform float time; 37 | uniform float radius; 38 | uniform float distort; 39 | ${distort} 40 | ${shader.vertexShader} 41 | ` 42 | shader.vertexShader = shader.vertexShader.replace( 43 | '#include ', 44 | ` 45 | float updateTime = time / 50.0; 46 | float noise = snoise(vec3(position / 2.0 + updateTime * 5.0)); 47 | vec3 transformed = vec3(position * (noise * pow(distort, 2.0) + radius)); 48 | ` 49 | ) 50 | } 51 | 52 | get time() { 53 | return this._time.value 54 | } 55 | 56 | set time(v) { 57 | this._time.value = v 58 | } 59 | 60 | get distort() { 61 | return this._distort.value 62 | } 63 | 64 | set distort(v) { 65 | this._distort.value = v 66 | } 67 | 68 | get radius() { 69 | return this._radius.value 70 | } 71 | 72 | set radius(v) { 73 | this._radius.value = v 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/materials/MeshReflectorMaterial.ts: -------------------------------------------------------------------------------- 1 | import { Matrix4, MeshStandardMaterial, Texture } from 'three' 2 | 3 | type UninitializedUniform = { value: Value | null } 4 | 5 | export class MeshReflectorMaterial extends MeshStandardMaterial { 6 | private _tDepth: UninitializedUniform = { value: null } 7 | private _distortionMap: UninitializedUniform = { value: null } 8 | private _tDiffuse: UninitializedUniform = { value: null } 9 | private _tDiffuseBlur: UninitializedUniform = { value: null } 10 | private _textureMatrix: UninitializedUniform = { value: null } 11 | private _hasBlur: { value: boolean } = { value: false } 12 | private _mirror: { value: number } = { value: 0.0 } 13 | private _mixBlur: { value: number } = { value: 0.0 } 14 | private _blurStrength: { value: number } = { value: 0.5 } 15 | private _minDepthThreshold: { value: number } = { value: 0.9 } 16 | private _maxDepthThreshold: { value: number } = { value: 1 } 17 | private _depthScale: { value: number } = { value: 0 } 18 | private _depthToBlurRatioBias: { value: number } = { value: 0.25 } 19 | private _distortion: { value: number } = { value: 1 } 20 | private _mixContrast: { value: number } = { value: 1.0 } 21 | 22 | constructor(parameters = {}) { 23 | super() 24 | 25 | this._tDepth = { value: null } 26 | this._distortionMap = { value: null } 27 | this._tDiffuse = { value: null } 28 | this._tDiffuseBlur = { value: null } 29 | this._textureMatrix = { value: null } 30 | this._hasBlur = { value: false } 31 | this._mirror = { value: 0.0 } 32 | this._mixBlur = { value: 0.0 } 33 | this._blurStrength = { value: 0.5 } 34 | this._minDepthThreshold = { value: 0.9 } 35 | this._maxDepthThreshold = { value: 1 } 36 | this._depthScale = { value: 0 } 37 | this._depthToBlurRatioBias = { value: 0.25 } 38 | this._distortion = { value: 1 } 39 | this._mixContrast = { value: 1.0 } 40 | this.setValues(parameters) 41 | } 42 | 43 | onBeforeCompile(shader) { 44 | if (!shader.defines?.USE_UV) { 45 | shader.defines.USE_UV = '' 46 | } 47 | shader.uniforms.hasBlur = this._hasBlur 48 | shader.uniforms.tDiffuse = this._tDiffuse 49 | shader.uniforms.tDepth = this._tDepth 50 | shader.uniforms.distortionMap = this._distortionMap 51 | shader.uniforms.tDiffuseBlur = this._tDiffuseBlur 52 | shader.uniforms.textureMatrix = this._textureMatrix 53 | shader.uniforms.mirror = this._mirror 54 | shader.uniforms.mixBlur = this._mixBlur 55 | shader.uniforms.mixStrength = this._blurStrength 56 | shader.uniforms.minDepthThreshold = this._minDepthThreshold 57 | shader.uniforms.maxDepthThreshold = this._maxDepthThreshold 58 | shader.uniforms.depthScale = this._depthScale 59 | shader.uniforms.depthToBlurRatioBias = this._depthToBlurRatioBias 60 | shader.uniforms.distortion = this._distortion 61 | shader.uniforms.mixContrast = this._mixContrast 62 | shader.vertexShader = ` 63 | uniform mat4 textureMatrix; 64 | varying vec4 my_vUv; 65 | ${shader.vertexShader}` 66 | shader.vertexShader = shader.vertexShader.replace( 67 | '#include ', 68 | `#include 69 | my_vUv = textureMatrix * vec4( position, 1.0 ); 70 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );` 71 | ) 72 | shader.fragmentShader = ` 73 | uniform sampler2D tDiffuse; 74 | uniform sampler2D tDiffuseBlur; 75 | uniform sampler2D tDepth; 76 | uniform sampler2D distortionMap; 77 | uniform float distortion; 78 | uniform float cameraNear; 79 | uniform float cameraFar; 80 | uniform bool hasBlur; 81 | uniform float mixBlur; 82 | uniform float mirror; 83 | uniform float mixStrength; 84 | uniform float minDepthThreshold; 85 | uniform float maxDepthThreshold; 86 | uniform float mixContrast; 87 | uniform float depthScale; 88 | uniform float depthToBlurRatioBias; 89 | varying vec4 my_vUv; 90 | ${shader.fragmentShader}` 91 | shader.fragmentShader = shader.fragmentShader.replace( 92 | '#include ', 93 | `#include 94 | 95 | float distortionFactor = 0.0; 96 | #ifdef USE_DISTORTION 97 | distortionFactor = texture2D(distortionMap, vUv).r * distortion; 98 | #endif 99 | 100 | vec4 new_vUv = my_vUv; 101 | new_vUv.x += distortionFactor; 102 | new_vUv.y += distortionFactor; 103 | 104 | vec4 base = texture2DProj(tDiffuse, new_vUv); 105 | vec4 blur = texture2DProj(tDiffuseBlur, new_vUv); 106 | 107 | vec4 merge = base; 108 | 109 | #ifdef USE_NORMALMAP 110 | vec2 normal_uv = vec2(0.0); 111 | vec4 normalColor = texture2D(normalMap, vUv * normalScale); 112 | vec3 my_normal = normalize( vec3( normalColor.r * 2.0 - 1.0, normalColor.b, normalColor.g * 2.0 - 1.0 ) ); 113 | vec3 coord = new_vUv.xyz / new_vUv.w; 114 | normal_uv = coord.xy + coord.z * my_normal.xz * 0.05; 115 | vec4 base_normal = texture2D(tDiffuse, normal_uv); 116 | vec4 blur_normal = texture2D(tDiffuseBlur, normal_uv); 117 | merge = base_normal; 118 | blur = blur_normal; 119 | #endif 120 | 121 | float depthFactor = 0.0001; 122 | float blurFactor = 0.0; 123 | 124 | #ifdef USE_DEPTH 125 | vec4 depth = texture2DProj(tDepth, new_vUv); 126 | depthFactor = smoothstep(minDepthThreshold, maxDepthThreshold, 1.0-(depth.r * depth.a)); 127 | depthFactor *= depthScale; 128 | depthFactor = max(0.0001, min(1.0, depthFactor)); 129 | 130 | #ifdef USE_BLUR 131 | blur = blur * min(1.0, depthFactor + depthToBlurRatioBias); 132 | merge = merge * min(1.0, depthFactor + 0.5); 133 | #else 134 | merge = merge * depthFactor; 135 | #endif 136 | 137 | #endif 138 | 139 | float reflectorRoughnessFactor = roughness; 140 | #ifdef USE_ROUGHNESSMAP 141 | vec4 reflectorTexelRoughness = texture2D( roughnessMap, vUv ); 142 | reflectorRoughnessFactor *= reflectorTexelRoughness.g; 143 | #endif 144 | 145 | #ifdef USE_BLUR 146 | blurFactor = min(1.0, mixBlur * reflectorRoughnessFactor); 147 | merge = mix(merge, blur, blurFactor); 148 | #endif 149 | 150 | vec4 newMerge = vec4(0.0, 0.0, 0.0, 1.0); 151 | newMerge.r = (merge.r - 0.5) * mixContrast + 0.5; 152 | newMerge.g = (merge.g - 0.5) * mixContrast + 0.5; 153 | newMerge.b = (merge.b - 0.5) * mixContrast + 0.5; 154 | 155 | diffuseColor.rgb = diffuseColor.rgb * ((1.0 - min(1.0, mirror)) + newMerge.rgb * mixStrength); 156 | ` 157 | ) 158 | } 159 | get tDiffuse(): Texture | null { 160 | return this._tDiffuse.value 161 | } 162 | set tDiffuse(v: Texture | null) { 163 | this._tDiffuse.value = v 164 | } 165 | get tDepth(): Texture | null { 166 | return this._tDepth.value 167 | } 168 | set tDepth(v: Texture | null) { 169 | this._tDepth.value = v 170 | } 171 | get distortionMap(): Texture | null { 172 | return this._distortionMap.value 173 | } 174 | set distortionMap(v: Texture | null) { 175 | this._distortionMap.value = v 176 | } 177 | get tDiffuseBlur(): Texture | null { 178 | return this._tDiffuseBlur.value 179 | } 180 | set tDiffuseBlur(v: Texture | null) { 181 | this._tDiffuseBlur.value = v 182 | } 183 | get textureMatrix(): Matrix4 | null { 184 | return this._textureMatrix.value 185 | } 186 | set textureMatrix(v: Matrix4 | null) { 187 | this._textureMatrix.value = v 188 | } 189 | get hasBlur(): boolean { 190 | return this._hasBlur.value 191 | } 192 | set hasBlur(v: boolean) { 193 | this._hasBlur.value = v 194 | } 195 | get mirror(): number { 196 | return this._mirror.value 197 | } 198 | set mirror(v: number) { 199 | this._mirror.value = v 200 | } 201 | get mixBlur(): number { 202 | return this._mixBlur.value 203 | } 204 | set mixBlur(v: number) { 205 | this._mixBlur.value = v 206 | } 207 | get mixStrength(): number { 208 | return this._blurStrength.value 209 | } 210 | set mixStrength(v: number) { 211 | this._blurStrength.value = v 212 | } 213 | get minDepthThreshold(): number { 214 | return this._minDepthThreshold.value 215 | } 216 | set minDepthThreshold(v: number) { 217 | this._minDepthThreshold.value = v 218 | } 219 | get maxDepthThreshold(): number { 220 | return this._maxDepthThreshold.value 221 | } 222 | set maxDepthThreshold(v: number) { 223 | this._maxDepthThreshold.value = v 224 | } 225 | get depthScale(): number { 226 | return this._depthScale.value 227 | } 228 | set depthScale(v: number) { 229 | this._depthScale.value = v 230 | } 231 | get depthToBlurRatioBias(): number { 232 | return this._depthToBlurRatioBias.value 233 | } 234 | set depthToBlurRatioBias(v: number) { 235 | this._depthToBlurRatioBias.value = v 236 | } 237 | get distortion(): number { 238 | return this._distortion.value 239 | } 240 | set distortion(v: number) { 241 | this._distortion.value = v 242 | } 243 | get mixContrast(): number { 244 | return this._mixContrast.value 245 | } 246 | set mixContrast(v: number) { 247 | this._mixContrast.value = v 248 | } 249 | } 250 | 251 | export type MeshReflectorMaterialProps = { 252 | mixBlur: number 253 | mixStrength: number 254 | mirror: number 255 | textureMatrix: Matrix4 256 | tDiffuse: Texture 257 | distortionMap?: Texture 258 | tDiffuseBlur: Texture 259 | hasBlur: boolean 260 | minDepthThreshold: number 261 | maxDepthThreshold: number 262 | depthScale: number 263 | depthToBlurRatioBias: number 264 | distortion: number 265 | mixContrast: number 266 | } 267 | -------------------------------------------------------------------------------- /src/materials/MeshWobbleMaterial.ts: -------------------------------------------------------------------------------- 1 | import { IUniform, MeshStandardMaterial, MeshStandardMaterialParameters } from 'three' 2 | 3 | export interface MeshWobbleMaterialParameters { 4 | time?: number 5 | factor?: number 6 | } 7 | 8 | export class MeshWobbleMaterial extends MeshStandardMaterial { 9 | _time: IUniform 10 | _factor: IUniform 11 | 12 | constructor({ 13 | time = 0, 14 | factor = 1, 15 | ...parameters 16 | }: MeshStandardMaterialParameters & MeshWobbleMaterialParameters = {}) { 17 | super(parameters) 18 | this.setValues(parameters) 19 | this._time = { value: time } 20 | this._factor = { value: factor } 21 | } 22 | 23 | // FIXME Use `THREE.WebGLProgramParametersWithUniforms` type when able to target @types/three@0.160.0 24 | override onBeforeCompile(shader: { vertexShader: string; uniforms: { [uniform: string]: IUniform } }) { 25 | shader.uniforms['time'] = this._time 26 | shader.uniforms['factor'] = this._factor 27 | 28 | shader.vertexShader = ` 29 | uniform float time; 30 | uniform float factor; 31 | ${shader.vertexShader} 32 | ` 33 | shader.vertexShader = shader.vertexShader.replace( 34 | '#include ', 35 | `float theta = sin( time + position.y ) / 2.0 * factor; 36 | float c = cos( theta ); 37 | float s = sin( theta ); 38 | mat3 m = mat3( c, 0, s, 0, 1, 0, -s, 0, c ); 39 | vec3 transformed = vec3( position ) * m; 40 | vNormal = vNormal * m;` 41 | ) 42 | } 43 | 44 | get time() { 45 | return this._time.value 46 | } 47 | 48 | set time(v) { 49 | this._time.value = v 50 | } 51 | 52 | get factor() { 53 | return this._factor.value 54 | } 55 | 56 | set factor(v) { 57 | this._factor.value = v 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/materials/SpotLightMaterial.ts: -------------------------------------------------------------------------------- 1 | import { Color, Vector2, Vector3, type Texture, UniformsLib, UniformsUtils } from 'three' 2 | import { shaderMaterial } from '../core/shaderMaterial' 3 | import { version } from '../helpers/constants' 4 | 5 | type SpotLightMaterialProps = { 6 | depth: Texture | null 7 | opacity: number 8 | attenuation: number 9 | anglePower: number 10 | spotPosition: Vector3 11 | lightColor: Color 12 | cameraNear: number 13 | cameraFar: number 14 | resolution: Vector2 15 | transparent: boolean 16 | depthWrite: boolean 17 | } 18 | 19 | export const SpotLightMaterial = shaderMaterial( 20 | { 21 | depth: null, 22 | opacity: 1, 23 | attenuation: 2.5, 24 | anglePower: 12, 25 | spotPosition: new Vector3(0, 0, 0), 26 | lightColor: new Color('white'), 27 | cameraNear: 0, 28 | cameraFar: 1, 29 | resolution: new Vector2(0, 0), 30 | transparent: true, 31 | depthWrite: false, 32 | }, 33 | /* glsl */ ` 34 | varying vec3 vNormal; 35 | varying float vViewZ; 36 | varying float vIntensity; 37 | uniform vec3 spotPosition; 38 | uniform float attenuation; 39 | 40 | #include 41 | #include 42 | #include 43 | 44 | void main() { 45 | #include 46 | #include 47 | #include 48 | 49 | // compute intensity 50 | vNormal = normalize(normalMatrix * normal); 51 | vec4 worldPosition = modelMatrix * vec4(position, 1); 52 | vec4 viewPosition = viewMatrix * worldPosition; 53 | vViewZ = viewPosition.z; 54 | 55 | vIntensity = 1.0 - saturate(distance(worldPosition.xyz, spotPosition) / attenuation); 56 | 57 | gl_Position = projectionMatrix * viewPosition; 58 | 59 | #include 60 | }`, 61 | /* glsl */ ` 62 | varying vec3 vNormal; 63 | varying float vViewZ; 64 | varying float vIntensity; 65 | 66 | uniform vec3 lightColor; 67 | uniform float anglePower; 68 | uniform sampler2D depth; 69 | uniform vec2 resolution; 70 | uniform float cameraNear; 71 | uniform float cameraFar; 72 | uniform float opacity; 73 | 74 | #include 75 | #include 76 | #include 77 | 78 | float readDepth(sampler2D depthSampler, vec2 uv) { 79 | float fragCoordZ = texture(depthSampler, uv).r; 80 | 81 | // https://github.com/mrdoob/three.js/issues/23072 82 | #ifdef USE_LOGDEPTHBUF 83 | float viewZ = 1.0 - exp2(fragCoordZ * log(cameraFar + 1.0) / log(2.0)); 84 | #else 85 | float viewZ = perspectiveDepthToViewZ(fragCoordZ, cameraNear, cameraFar); 86 | #endif 87 | 88 | return viewZ; 89 | } 90 | 91 | void main() { 92 | #include 93 | 94 | vec3 normal = vec3(vNormal.x, vNormal.y, abs(vNormal.z)); 95 | float angleIntensity = pow(dot(normal, vec3(0, 0, 1)), anglePower); 96 | float intensity = vIntensity * angleIntensity; 97 | 98 | // fades when z is close to sampled depth, meaning the cone is intersecting existing geometry 99 | bool isSoft = resolution[0] > 0.0 && resolution[1] > 0.0; 100 | if (isSoft) { 101 | vec2 uv = gl_FragCoord.xy / resolution; 102 | intensity *= smoothstep(0.0, 1.0, vViewZ - readDepth(depth, uv)); 103 | } 104 | 105 | gl_FragColor = vec4(lightColor, intensity * opacity); 106 | 107 | #include 108 | #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> 109 | #include 110 | }`, 111 | (material) => { 112 | Object.assign(material.uniforms, UniformsUtils.merge([UniformsLib['fog']])) 113 | } 114 | ) 115 | -------------------------------------------------------------------------------- /src/materials/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MeshDiscardMaterial' 2 | export * from './MeshTransmissionMaterial' 3 | export * from './SpotLightMaterial' 4 | export * from './BlurPass' 5 | export * from './ConvolutionMaterial' 6 | export * from './MeshReflectorMaterial' 7 | export * from './MeshDistortMaterial' 8 | export * from './MeshWobbleMaterial' 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es2019", "dom"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "esModuleInterop": false, 8 | "allowSyntheticDefaultImports": false, 9 | "pretty": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "declaration": true, 13 | "removeComments": true, 14 | "emitDeclarationOnly": true, 15 | "outDir": "dist", 16 | "resolveJsonModule": true, 17 | "noImplicitAny": false, 18 | "noImplicitThis": false, 19 | "baseUrl": "./src" 20 | }, 21 | "include": ["./src", "custom.d.ts"], 22 | "exclude": ["./node_modules/**/*"] 23 | } 24 | --------------------------------------------------------------------------------