{"id":13456484,"url":"https://github.com/creativelifeform/three-nebula","last_synced_at":"2026-10-19T14:35:38.352Z","repository":{"id":37334628,"uuid":"157932493","full_name":"creativelifeform/three-nebula","owner":"creativelifeform","description":"WebGL based particle system engine for three.js","archived":false,"fork":false,"pushed_at":"2026-09-19T06:02:49.000Z","size":18375,"stargazers_count":1225,"open_issues_count":37,"forks_count":94,"subscribers_count":17,"default_branch":"master","last_synced_at":"2026-09-22T01:45:02.484Z","etag":null,"topics":["javascript","particles","threejs","webgl"],"latest_commit_sha":null,"homepage":"https://three-nebula.org/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/creativelifeform.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"claude":null,"gemini":null,"cursor":null,"copilot":null,"dco":null,"cla":null,"disclosure":null}},"created_at":"2018-11-16T23:27:56.000Z","updated_at":"2026-09-22T01:27:39.000Z","dependencies_parsed_at":"2026-09-14T00:22:59.872Z","dependency_job_id":null,"html_url":"https://github.com/creativelifeform/three-nebula","commit_stats":{"total_commits":372,"total_committers":9,"mean_commits":"41.333333333333336","dds":"0.18817204301075274","last_synced_commit":"b22cd14272b5b64c83b05a3c6ee68f9e924e46f4"},"previous_names":["rohan-deshpande/three-proton"],"tags_count":70,"template":false,"template_full_name":null,"purl":"pkg:github/creativelifeform/three-nebula","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/creativelifeform%2Fthree-nebula","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/creativelifeform%2Fthree-nebula/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/creativelifeform%2Fthree-nebula/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/creativelifeform%2Fthree-nebula/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/creativelifeform","download_url":"https://codeload.github.com/creativelifeform/three-nebula/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/creativelifeform%2Fthree-nebula/sbom","scorecard":{"id":308211,"data":{"date":"2025-08-11","repo":{"name":"github.com/creativelifeform/three-nebula","commit":"b22cd14272b5b64c83b05a3c6ee68f9e924e46f4"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.8,"checks":[{"name":"Code-Review","score":0,"reason":"Found 2/29 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/ci.yml:1","Warn: no topLevel permission defined: .github/workflows/publish.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.md:0","Info: FSF or OSI recognized license: MIT License: LICENSE.md:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/creativelifeform/three-nebula/ci.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/creativelifeform/three-nebula/ci.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:28: update your workflow using https://app.stepsecurity.io/secureworkflow/creativelifeform/three-nebula/ci.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/creativelifeform/three-nebula/publish.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/creativelifeform/three-nebula/publish.yml/master?enable=pin","Warn: npmCommand not pinned by hash: scripts/docs-build:6","Warn: npmCommand not pinned by hash: .github/workflows/ci.yml:23","Warn: npmCommand not pinned by hash: .github/workflows/publish.yml:20","Info:   0 out of   4 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned","Info:   0 out of   3 npmCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 9 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"65 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-wf5p-g6vw-rhxx","Warn: Project is vulnerable to: GHSA-jr5f-v2jv-69x6","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-jchw-25xp-jwwc","Warn: Project is vulnerable to: GHSA-cxjh-pqwp-8mfp","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-4q6p-r6v2-jvc5","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-76p3-8jx3-jpfq","Warn: Project is vulnerable to: GHSA-3rfm-jhwj-7488","Warn: Project is vulnerable to: GHSA-hhq3-ff78-jv3g","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55","Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j","Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-76p7-773f-r4q5","Warn: Project is vulnerable to: GHSA-pq67-2wwv-3xjx","Warn: Project is vulnerable to: GHSA-8cj5-5rvv-wf4v","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-hc6q-2mpp-qw7j","Warn: Project is vulnerable to: GHSA-4vvj-4cpr-p986","Warn: Project is vulnerable to: GHSA-j8xg-fqg3-53r7","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q","Warn: Project is vulnerable to: GHSA-v88g-cgmw-v5xw","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-qwcr-r2fm-qrc7","Warn: Project is vulnerable to: GHSA-x9w5-v3q2-3rhw","Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x","Warn: Project is vulnerable to: GHSA-phwq-j96m-2c2q","Warn: Project is vulnerable to: GHSA-ghr5-ch3p-vcr6","Warn: Project is vulnerable to: GHSA-434g-2637-qmqr","Warn: Project is vulnerable to: GHSA-49q7-c7j4-3p7m","Warn: Project is vulnerable to: GHSA-977x-g7h5-7qgw","Warn: Project is vulnerable to: GHSA-f7q4-pwc6-w24p","Warn: Project is vulnerable to: GHSA-fc9h-whq2-v747","Warn: Project is vulnerable to: GHSA-vjh7-7g9h-fjfh","Warn: Project is vulnerable to: GHSA-rv95-896h-c2vc","Warn: Project is vulnerable to: GHSA-qw6h-vgh9-j6wx","Warn: Project is vulnerable to: GHSA-896r-f27r-55mw","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-qrpm-p2h7-hrv2","Warn: Project is vulnerable to: GHSA-25mp-g6fv-mqxx","Warn: Project is vulnerable to: GHSA-wr66-vrwm-5g5x","Warn: Project is vulnerable to: GHSA-fmvm-x8mv-47mj","Warn: Project is vulnerable to: GHSA-f82v-jwr5-mffw","Warn: Project is vulnerable to: GHSA-c59h-r6p8-q9wc","Warn: Project is vulnerable to: GHSA-g77x-44xx-532m","Warn: Project is vulnerable to: GHSA-7gfc-8cq8-jh5f","Warn: Project is vulnerable to: GHSA-qpjv-v59x-3qc4","Warn: Project is vulnerable to: GHSA-r683-j2x4-v87g","Warn: Project is vulnerable to: GHSA-rhx6-c78j-4q9w","Warn: Project is vulnerable to: GHSA-h7cp-r72f-jxh6","Warn: Project is vulnerable to: GHSA-v62p-rq8g-8h59","Warn: Project is vulnerable to: GHSA-7fh5-64p2-3v2j","Warn: Project is vulnerable to: GHSA-m6fv-jmcg-4jfg","Warn: Project is vulnerable to: GHSA-cm22-4g7w-348p","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-17T22:36:49.257Z","repository_id":37334628,"created_at":"2025-08-17T22:36:49.257Z","updated_at":"2025-08-17T22:36:49.257Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":344872684,"owners_count":38358115,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-10-03T21:59:58.778Z","status":"online","status_checked_at":"2026-10-19T02:00:06.414Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript","particles","threejs","webgl"],"created_at":"2024-07-31T08:01:22.858Z","updated_at":"2026-10-19T14:35:38.342Z","avatar_url":"https://github.com/creativelifeform.png","language":"JavaScript","funding_links":[],"categories":["Libraries","JavaScript"],"sub_categories":["JavaScript"],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://three-nebula.org/\"\u003e\n    \u003cimg alt=\"react-router\" src=\"https://three-nebula.org/favicon/nebula-logo-favicon-master.svg\" width=\"100\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003ch3 align=\"center\"\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;three-nebula\u003c/h3\u003e\n\n\u003cp align=\"center\"\u003e\n  \u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;WebGL based 3D particle system engine for \u003ca href=\"https://threejs.org\"\u003ethree\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\n  \u003ca href=\"https://github.com/creativelifeform/three-nebula/actions?query=workflow%3Aci\"\u003e\u003cimg src=\"https://github.com/creativelifeform/three-nebula/workflows/ci/badge.svg\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://coveralls.io/github/creativelifeform/three-nebula?branch=master\u0026kill_cache=1\"\u003e\u003cimg src=\"https://coveralls.io/repos/github/creativelifeform/three-nebula/badge.svg\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://three-nebula-docs.netlify.app\"\u003e\u003cimg src=\"https://api.netlify.com/api/v1/badges/1d2dda8a-cc12-487c-950d-ff69d95c7355/deploy-status\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://threejs.org\"\u003e\u003cimg src=\"https://img.shields.io/badge/three-v0.185.1-%230C7BB8\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.typescriptlang.org/\"\u003e\u003cimg src=\"https://img.shields.io/badge/TypeScript-3178C6?logo=typescript\u0026logoColor=white\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003chr/\u003e\n\n[CHANGELOG](https://github.com/creativelifeform/three-nebula/blob/master/CHANGELOG.md)\n\n**Three Nebula** is a WebGL based 3D particle engine that has been designed to work alongside [`three.js`](https://github.com/mrdoob/three.js). Check out the [website](https://three-nebula.org/), [examples](https://three-nebula.org/examples), the [quickstart sandbox](https://codesandbox.io/s/three-nebula-quickstart-kz6uv) and [API reference documentation](https://three-nebula-docs.netlify.app/) for more.\n\n## Features\n\n- Built and tested against [`three@0.185.1`](https://github.com/mrdoob/three.js)\n- The ability to instantiate `three-nebula` particle systems from JSON objects\n- The ability to create particle systems from sprites as well as 3D meshes\n- Many kinds of particle behaviours and initializers\n- Optional WebGPU rendering via a batched `GPURenderer` at [`three-nebula/webgpu`](#webgpu)\n\n## Installation\n\n### npm\n\n```\nnpm i --save three-nebula\n```\n\n### script\n\n```\n\u003cscript type='text/javascript' src='node_modules/three-nebula/dist/three-nebula.umd.js'\u003e\u003c/script\u003e\n```\n\n## Usage\n\n`three-nebula` ships ES module, CommonJS and UMD builds and declares [`three`](https://github.com/mrdoob/three.js) as a peer dependency, so install both alongside each other:\n\n```\nnpm i --save three three-nebula\n```\n\nIt works with any bundler (Vite, webpack, Rollup) or straight from a `\u003cscript\u003e` tag — see the examples below. However you build a system, the one thing to remember is to **drive it from your render loop by calling `system.update()` once per frame**; nothing animates until you do. For runnable, self-contained examples of every renderer, see the [sandbox](#sandbox).\n\n### Module\n\n```javascript\nimport * as THREE from 'three';\n\nimport System, {\n  SpriteRenderer,\n  Emitter,\n  Rate,\n  Span,\n  Position,\n  Mass,\n  Radius,\n  Life,\n  RadialVelocity,\n  Vector3D,\n  Alpha,\n  Scale,\n  Color,\n  PointZone,\n} from 'three-nebula';\n\nconst system = new System();\nconst renderer = new SpriteRenderer(threeScene, THREE);\nconst emitter = new Emitter();\n\n// Set emitter rate (particles per second) as well as the particle initializers and behaviours\nemitter\n  .setRate(new Rate(new Span(4, 16), new Span(0.01)))\n  .setInitializers([\n    new Position(new PointZone(0, 0)),\n    new Mass(1),\n    new Radius(6, 12),\n    new Life(3),\n    new RadialVelocity(45, new Vector3D(0, 1, 0), 180),\n  ])\n  .setBehaviours([\n    new Alpha(1, 0),\n    new Scale(0.1, 1.3),\n    new Color(new THREE.Color(0xff0000), new THREE.Color(0x0000ff)),\n  ])\n  .emit();\n\n// add the emitter and a renderer to your particle system\nsystem\n  .addRenderer(renderer)\n  .addEmitter(emitter)\n  .emit({\n    onStart: () =\u003e {},\n    onUpdate: () =\u003e {},\n    onEnd: () =\u003e {},\n  });\n\n// drive the system from your render loop\nconst animate = () =\u003e {\n  system.update();\n  requestAnimationFrame(animate);\n};\nrequestAnimationFrame(animate);\n```\n\nYou can also instantiate your system from a JSON object\n\n```javascript\nimport System from 'three-nebula';\n\nconst json = {\n  preParticles: 500,\n  integrationType: 'euler',\n  emitters: [\n    {\n      rate: {\n        particlesMin: 5,\n        particlesMax: 7,\n        perSecondMin: 0.01,\n        perSecondMax: 0.02,\n      },\n      position: {\n        x: 70,\n        y: 0,\n      },\n      initializers: [\n        {\n          type: 'Mass',\n          properties: {\n            min: 1,\n            max: 1,\n          },\n        },\n        {\n          type: 'Life',\n          properties: {\n            min: 2,\n            max: 2,\n          },\n        },\n        {\n          type: 'BodySprite',\n          properties: {\n            texture: './img/dot.png',\n          },\n        },\n        {\n          type: 'Radius',\n          properties: {\n            width: 80,\n            height: 80,\n          },\n        },\n      ],\n      behaviours: [\n        {\n          type: 'Alpha',\n          properties: {\n            alphaA: 1,\n            alphaB: 0,\n          },\n        },\n        {\n          type: 'Color',\n          properties: {\n            colorA: '#4F1500',\n            colorB: '#0029FF',\n          },\n        },\n        {\n          type: 'Scale',\n          properties: {\n            scaleA: 1,\n            scaleB: 0.5,\n          },\n        },\n        {\n          type: 'Force',\n          properties: {\n            fx: 0,\n            fy: 0,\n            fz: -20,\n          },\n        },\n      ],\n    },\n    {\n      rate: {\n        particlesMin: 5,\n        particlesMax: 7,\n        perSecondMin: 0.01,\n        perSecondMax: 0.02,\n      },\n      position: {\n        x: -70,\n        y: 0,\n      },\n      initializers: [\n        {\n          type: 'Mass',\n          properties: {\n            min: 1,\n            max: 1,\n          },\n        },\n        {\n          type: 'Life',\n          properties: {\n            min: 2,\n            max: 2,\n          },\n        },\n        {\n          type: 'BodySprite',\n          properties: {\n            texture: './img/dot.png',\n          },\n        },\n        {\n          type: 'Radius',\n          properties: {\n            width: 80,\n            height: 80,\n          },\n        },\n      ],\n      behaviours: [\n        {\n          type: 'Alpha',\n          properties: {\n            alphaA: 1,\n            alphaB: 0,\n          },\n        },\n        {\n          type: 'Color',\n          properties: {\n            colorA: '#004CFE',\n            colorB: '#6600FF',\n          },\n        },\n        {\n          type: 'Scale',\n          properties: {\n            scaleA: 1,\n            scaleB: 0.5,\n          },\n        },\n        {\n          type: 'Force',\n          properties: {\n            fx: 0,\n            fy: 0,\n            fz: -20,\n          },\n        },\n      ],\n    },\n  ],\n};\n\nSystem.fromJSONAsync(json, THREE).then(system =\u003e {\n  console.log(system);\n});\n```\n\n### WebGPU\n\n`three-nebula` ships an optional batched `GPURenderer` for WebGPU at the `three-nebula/webgpu` entry point. It draws every particle as a camera-facing instanced quad in a single draw call and packs multiple textures into an atlas, and is a drop-in alternative to `SpriteRenderer` when your app renders with three's `WebGPURenderer`:\n\n```javascript\nimport * as THREE from 'three/webgpu';\nimport System, {\n  Emitter /* … initializers, behaviours … */,\n} from 'three-nebula';\nimport { GPURenderer } from 'three-nebula/webgpu';\n\nconst renderer = new THREE.WebGPURenderer();\nawait renderer.init();\n\nconst system = new System();\nsystem.addRenderer(new GPURenderer(scene, THREE));\n// build emitters as usual, then drive system.update() from your render loop\n```\n\n\u003e **Requires a modern `three`.** The WebGPU entry point imports `three/webgpu` and `three/tsl`, which only exist in recent `three` releases (roughly r167+). This requirement applies **only** if you import `three-nebula/webgpu` — the core `three-nebula` package's supported `three` range is unchanged.\n\n### Script Tag\n\nIf you are adding `three-nebula` to your project in the script tag, the only difference to the above example is how you access the classes you need. You can do that like so\n\n```javascript\nconst { System, Emitter, Rate, Span } = window.Nebula;\nconst system = new System();\n```\n\n## Determinism \u0026 seeding\n\nEvery system is driven by a **seeded** pseudo-random generator, so a simulation can be reproduced exactly — the same seed produces the same result, on any machine.\n\n**By default the seed is random**, so systems still vary from run to run and you don't have to change anything. For reproducible output, set a seed:\n\n```javascript\nconst system = new System();\n\nsystem.setSeed(1234); // deterministic from here on\n// …add emitters, emit as usual…\n```\n\n**Which method should I use?** Seeding and stepping are two independent choices — `setSeed` controls _what_ randomness is drawn; `update`/`tick` control _how time advances_. Pick a stepping method by what you're doing:\n\n| Your situation                                             | Use                                | Why                                                                     |\n| ---------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------- |\n| **Live rendering in a browser** (most apps)                | `tick(realDeltaSeconds)`           | Refresh-rate independent — correct speed on any display, stall-safe     |\n| **Reproducible/offline**: tests, thumbnails, export, replays | `update()` a fixed number of times | Byte-identical output, no wall-clock involved                           |\n| **Inside your own fixed-timestep game loop**               | `update(dt)` with your own `dt`    | Particles advance in lockstep with your sim (don't nest two accumulators) |\n\n**The short version: reach for `tick` when rendering live** — it's the one most apps want, and it avoids the \"runs 2× too fast on a 120Hz display\" trap. **Reach for `update` only when you need exact, reproducible stepping** (or you're driving from your own loop). Add `setSeed(...)` on top of _either_ when you want the effect to look the same every time it plays — e.g. a curated preview or thumbnail. Leave the seed unset for ambient effects that can vary run to run.\n\n\u003e For a **guaranteed** byte-identical replay, prefer `update()` × N: `tick` is deterministic for the same total elapsed time, but a long stall can hit `maxSubSteps` and drop time, nudging live playback off a previous run.\n\n**Reproducibility = same seed + the same number of fixed steps.** Drive the sim by calling `system.update()` a fixed number of times — each call advances one fixed `1/60s` step — and the result is byte-identical every run:\n\n```javascript\nsystem.setSeed(1234);\nfor (let i = 0; i \u003c 300; i++) system.update(); // identical across runs\n```\n\n**Real-time playback.** For a live render loop driven by `requestAnimationFrame`, call `system.tick(realDeltaSeconds)` instead of `update()`. It advances the sim in fixed steps based on **real elapsed time**, so playback runs at the correct speed regardless of the display's refresh rate — whereas calling `update()` once per frame ties speed to how often it's called (≈2× too fast on a 120Hz display). Long stalls (e.g. a backgrounded tab) are clamped so the sim can't spiral (see the next section to tune this).\n\n```javascript\nconst loop = (now, last = now) =\u003e {\n  system.tick((now - last) / 1000); // real seconds since last frame\n  renderer.render(scene, camera);\n  requestAnimationFrame(next =\u003e loop(next, now));\n};\nrequestAnimationFrame(loop);\n```\n\n\u003e **Upgrading an existing project?** Two behaviour changes are worth knowing:\n\u003e\n\u003e - **Already calling `system.update()` once per frame?** That's the legacy pattern that assumes a 60Hz display — it runs ~2× too fast on a 120Hz screen. Switch your `requestAnimationFrame` loop to `system.tick(realDeltaSeconds)` for refresh-rate-independent speed. (Keep `update()` only for deterministic/offline stepping or your own fixed loop.)\n\u003e - **Seeding `Math.random` globally to make particles reproducible?** That no longer works — the engine now draws from its own isolated stream and doesn't touch `Math.random`. Use `system.setSeed(...)` instead, which is the supported (and much stronger) way to get reproducible output.\n\n**Tuning the loop: `fixedTimeStep` and `maxSubSteps`.** `tick` turns real elapsed time into whole fixed steps using two knobs on the system:\n\n- **`system.fixedTimeStep`** (default `1/60`s ≈ `0.0167`) — the size of one simulation step, in seconds. `tick` accumulates real time and runs one `update(fixedTimeStep)` for each whole step that fits, carrying the leftover into the next call. It's also the step `update()` uses when called with no argument. Smaller steps give smoother, more accurate motion but do more work per second; larger steps are cheaper but chunkier. **Reproducibility is defined relative to this value** — two runs match only if they use the same `fixedTimeStep` and the same number of steps.\n- **`system.maxSubSteps`** (default `6`) — the most steps a single `tick` call will run. This caps catch-up after a long frame or stall (say a backgrounded tab that resumes with a multi-second delta). Without a cap, a huge delta would try to run hundreds of updates in one frame, and each frame would then fall further behind — the \"spiral of death\". When the cap is hit, the leftover time is dropped so the sim skips ahead instead of freezing.\n\nTogether they bound the work per `tick`: at most `maxSubSteps` updates, i.e. `fixedTimeStep × maxSubSteps` seconds of simulation. With the defaults that's `6 × 1/60 = 0.1s` — any single frame longer than 100ms of real time has its excess discarded rather than simulated.\n\n```javascript\nsystem.fixedTimeStep = 1 / 120; // finer, smoother steps (more CPU per second)\nsystem.maxSubSteps = 10; //        allow more catch-up before dropping time\n```\n\nThese only affect `tick`. `update(dt)` always advances by exactly the `dt` you pass (or one `fixedTimeStep` if you pass nothing), so deterministic stepping is unaffected by `maxSubSteps`.\n\n**Using it in a game.** Call `system.update(dt)` from your own update loop — a game that already runs a fixed-timestep loop uses `update`, not `tick`, so the particles advance in lockstep with your game's own steps (nesting `tick`'s accumulator inside yours would desync them). If you want the particle visuals to be part of your reproducible / replay world, seed the system from your game's own generator:\n\n```javascript\nsystem.setSeed(myGameRng.int32());\n```\n\n**Isolation.** The engine draws from its own per-system stream and does **not** consume from the global `Math.random`. If you seed `Math.random` globally for your own determinism, particle draws won't disturb your sequence.\n\n**Scope.** Determinism holds within one JavaScript engine on one platform — it is not intended for cross-machine lockstep netcode. Particles are visual state; keep them on the presentation side of a netcode boundary.\n\n## Additive particles \u0026 transparent canvases\n\nAdditive blending adds light to whatever is already in the WebGL framebuffer. That works\nperfectly on an **opaque** canvas — but a `THREE.WebGLRenderer` created with `{ alpha: true }`\n(a transparent canvas over a DOM/CSS background) is a common gotcha, because WebGL can't\nadditively blend with the page behind the canvas — the canvas is composited **over** the page,\nnot added to it. Two rules keep additive particles looking right ([#133](https://github.com/creativelifeform/three-nebula/issues/133)):\n\n1. **Over a transparent canvas, use textures that have an alpha channel.** The alpha is the\n   coverage the browser needs to composite correctly. A fully-opaque, no-alpha,\n   black-background additive texture will render as **opaque squares**, because its corners are\n   opaque. (The classic three.js sprite textures such as `disc.png` all carry alpha.)\n\n2. **For a flat colour or gradient background, let three own it — render it in the scene**\n   (`scene.background`, or a backdrop mesh) on an **opaque** canvas, rather than a CSS\n   background behind a transparent canvas. With the background in the framebuffer, additive\n   blends against real pixels and works with **any** texture, no alpha channel required:\n\n   ```javascript\n   // gradient (or flat colour, image, …) as the scene background — opaque canvas\n   scene.background = myGradientTexture;\n   ```\n\n   See the `Additive Blending — Scene Background` sandbox experiments (CPU + GPU) for a\n   working example, and [#133](https://github.com/creativelifeform/three-nebula/issues/133)\n   for the full rationale.\n\n## Development\n\n### Sandbox\n\nThe sandbox in `./sandbox` is a small collection of visual experiments for testing and playing with library changes — the kind of barebones examples that make it easy to dig into a rendering issue or try something new. The experiments aren't permanent; they get added and removed over time.\n\nRun it with\n\n```\nnpm run sandbox\n```\n\nThis serves the sandbox with Vite (defaults to `http://localhost:5000`, falling back to the next free port). Pick an experiment from the index page.\n\nEach experiment is a small ES module — there's no build config to think about. Vite resolves `three`, `three/addons/*` and `three-nebula` by name, and `three-nebula` is aliased to the library **source**, so editing the library hot-reloads the sandbox with no separate build step.\n\nAdding an experiment is just two files under `sandbox/experiments/\u003cname\u003e/`.\n\n`index.html` — the shared styles, a canvas inside an `#app` container (the harness mounts its FPS panel there and the styles size the canvas), and a module entry point:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"UTF-8\" /\u003e\n    \u003clink rel=\"stylesheet\" href=\"/style/reset.css\" /\u003e\n    \u003clink rel=\"stylesheet\" href=\"/style/app.css\" /\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv id=\"app\"\u003e\n      \u003ccanvas id=\"canvas\"\u003e\u003c/canvas\u003e\n    \u003c/div\u003e\n    \u003cscript type=\"module\" src=\"./index.js\"\u003e\u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n`index.js` — build a system and hand it to `run`:\n\n```javascript\nimport * as THREE from 'three';\nimport System, { Emitter, SpriteRenderer /* … */ } from 'three-nebula';\nimport { run } from '/common/run.js';\n\nconst init = async ({ scene, camera, renderer }) =\u003e {\n  const system = new System();\n  // … set up emitters, initializers and behaviours …\n  return system.addRenderer(new SpriteRenderer(scene, THREE));\n};\n\nrun(init);\n```\n\n`run` (in `sandbox/common/`) sets up the scene, camera, renderer and animation loop, calls your `init` with `{ scene, camera, renderer }`, and drives `system.update()` every frame — so an experiment only has to describe the system it wants to see.\n\n### Visual regression testing\n\nThe golden-master visual regression (VR) suite lives in `./vr`. It renders the library's example scenes — kept in `sandbox/examples/`, decoupled from the docs website — through a deterministic headless harness and diffs each screenshot against a committed baseline.\n\n```\nnpm run vr           # render every example, check determinism (render twice, diff)\nnpm run vr:diff      # diff the latest render against the committed baselines\nnpm run vr:baseline  # (re)write the baselines in vr/baselines/\nnpm run vr:montage   # stitch the captures into one overview grid\n```\n\nDeterminism comes from the harness, not luck: it seeds the global RNG, pins `requestAnimationFrame`, and captures a fixed number of frames, so a given example renders the same pixels every run. Baselines are compared with `pixelmatch`, and the image files are tracked with Git LFS.\n\n**Why SwiftShader.** The captures run on headless Chromium with WebGL forced onto SwiftShader (Google's software rasteriser) via `--use-gl=angle --use-angle=swiftshader`. That's deliberate: the golden master is diffed near-pixel-exact and committed to the repo, and real GPUs don't produce identical pixels across machines. This is an OSS library — we can't pin the CI runner — so a hardware-rendered baseline would flake everywhere; SwiftShader renders bit-identical on any machine, which is what makes a committable baseline viable.\n\n**The trade-off.** SwiftShader does not render the `GPURenderer`'s point sprites faithfully (they come out blocky regardless of the real output), so VR is trustworthy for the CPU-material renderers (`SpriteRenderer` / `MeshRenderer`) but **blind to `GPURenderer` visual correctness** — validate GPU changes in a real/headed browser plus unit tests. See [`vr/README.md`](vr/README.md) for the full rationale.\n\n## License\n\n[MIT](LICENSE.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcreativelifeform%2Fthree-nebula","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcreativelifeform%2Fthree-nebula","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcreativelifeform%2Fthree-nebula/lists"}