diff --git a/.htaccess b/.htaccess
new file mode 100644
index 000000000..5e8e3d2e2
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,13 @@
+
+# Caching
+
+# 1 week for images, videos
+
+Header set Cache-Control "public, max-age=604800, no-transform"
+
+
+
+Header set Cache-Control "public, max-age=120, must-revalidate"
+
+
+
\ No newline at end of file
diff --git a/.preview/404.png b/.preview/404.png
new file mode 100644
index 000000000..2a4bd5eb0
Binary files /dev/null and b/.preview/404.png differ
diff --git a/.preview/backlog mermaid.png b/.preview/backlog mermaid.png
new file mode 100644
index 000000000..f9cc5e503
Binary files /dev/null and b/.preview/backlog mermaid.png differ
diff --git a/.preview/backlog.png b/.preview/backlog.png
new file mode 100644
index 000000000..449ef0126
Binary files /dev/null and b/.preview/backlog.png differ
diff --git a/.preview/bike configurator.png b/.preview/bike configurator.png
new file mode 100644
index 000000000..613aa1f8c
Binary files /dev/null and b/.preview/bike configurator.png differ
diff --git a/.preview/castle builder.png b/.preview/castle builder.png
new file mode 100644
index 000000000..d4b159438
Binary files /dev/null and b/.preview/castle builder.png differ
diff --git a/.preview/community: contributions.png b/.preview/community: contributions.png
new file mode 100644
index 000000000..9dac3ca56
Binary files /dev/null and b/.preview/community: contributions.png differ
diff --git a/.preview/contributions: kipash.png b/.preview/contributions: kipash.png
new file mode 100644
index 000000000..75aa59046
Binary files /dev/null and b/.preview/contributions: kipash.png differ
diff --git a/.preview/contributions: krisrok.png b/.preview/contributions: krisrok.png
new file mode 100644
index 000000000..6fb1eee2e
Binary files /dev/null and b/.preview/contributions: krisrok.png differ
diff --git a/.preview/contributions: llllkatjallll.png b/.preview/contributions: llllkatjallll.png
new file mode 100644
index 000000000..465f693df
Binary files /dev/null and b/.preview/contributions: llllkatjallll.png differ
diff --git a/.preview/contributions: marwie.png b/.preview/contributions: marwie.png
new file mode 100644
index 000000000..c66f8136e
Binary files /dev/null and b/.preview/contributions: marwie.png differ
diff --git a/.preview/contributions: robyer1.png b/.preview/contributions: robyer1.png
new file mode 100644
index 000000000..8beb7e09d
Binary files /dev/null and b/.preview/contributions: robyer1.png differ
diff --git a/.preview/contributions: web3kev.png b/.preview/contributions: web3kev.png
new file mode 100644
index 000000000..9c5744e9c
Binary files /dev/null and b/.preview/contributions: web3kev.png differ
diff --git a/.preview/core components.png b/.preview/core components.png
new file mode 100644
index 000000000..dc722a929
Binary files /dev/null and b/.preview/core components.png differ
diff --git a/.preview/deployment & compression.png b/.preview/deployment & compression.png
new file mode 100644
index 000000000..caa3b627e
Binary files /dev/null and b/.preview/deployment & compression.png differ
diff --git a/.preview/editor component generator.png b/.preview/editor component generator.png
new file mode 100644
index 000000000..9da86595a
Binary files /dev/null and b/.preview/editor component generator.png differ
diff --git a/.preview/everywhere actions.png b/.preview/everywhere actions.png
new file mode 100644
index 000000000..fc652d244
Binary files /dev/null and b/.preview/everywhere actions.png differ
diff --git a/.preview/examples.png b/.preview/examples.png
new file mode 100644
index 000000000..66e5a0e54
Binary files /dev/null and b/.preview/examples.png differ
diff --git a/.preview/exporting assets to gltf.png b/.preview/exporting assets to gltf.png
new file mode 100644
index 000000000..8b438a773
Binary files /dev/null and b/.preview/exporting assets to gltf.png differ
diff --git a/.preview/features overview.png b/.preview/features overview.png
new file mode 100644
index 000000000..67d405033
Binary files /dev/null and b/.preview/features overview.png differ
diff --git a/.preview/frameworks, bundlers, html.png b/.preview/frameworks, bundlers, html.png
new file mode 100644
index 000000000..ed36fec13
Binary files /dev/null and b/.preview/frameworks, bundlers, html.png differ
diff --git a/.preview/getting started & installation.png b/.preview/getting started & installation.png
new file mode 100644
index 000000000..6fdb419a4
Binary files /dev/null and b/.preview/getting started & installation.png differ
diff --git a/.preview/getting started.png b/.preview/getting started.png
new file mode 100644
index 000000000..c14a4e134
Binary files /dev/null and b/.preview/getting started.png differ
diff --git a/.preview/how to debug.png b/.preview/how to debug.png
new file mode 100644
index 000000000..dc115e3cb
Binary files /dev/null and b/.preview/how to debug.png differ
diff --git a/.preview/kipash: calculate pointer world position.png b/.preview/kipash: calculate pointer world position.png
new file mode 100644
index 000000000..051eff815
Binary files /dev/null and b/.preview/kipash: calculate pointer world position.png differ
diff --git a/.preview/krisrok: always open in specific browser.png b/.preview/krisrok: always open in specific browser.png
new file mode 100644
index 000000000..4a2e7ee8d
Binary files /dev/null and b/.preview/krisrok: always open in specific browser.png differ
diff --git a/.preview/llllkatjallll: set fallback material for usdz exporter.png b/.preview/llllkatjallll: set fallback material for usdz exporter.png
new file mode 100644
index 000000000..bb9dbfc87
Binary files /dev/null and b/.preview/llllkatjallll: set fallback material for usdz exporter.png differ
diff --git a/.preview/marwie: camera video background.png b/.preview/marwie: camera video background.png
new file mode 100644
index 000000000..f915a0a81
Binary files /dev/null and b/.preview/marwie: camera video background.png differ
diff --git a/.preview/marwie: code contribution example.png b/.preview/marwie: code contribution example.png
new file mode 100644
index 000000000..e2bc9d667
Binary files /dev/null and b/.preview/marwie: code contribution example.png differ
diff --git a/.preview/marwie: control a timeline by scroll.png b/.preview/marwie: control a timeline by scroll.png
new file mode 100644
index 000000000..5979a09f7
Binary files /dev/null and b/.preview/marwie: control a timeline by scroll.png differ
diff --git a/.preview/marwie: everywhere action emphasize on click.png b/.preview/marwie: everywhere action emphasize on click.png
new file mode 100644
index 000000000..35bc9519e
Binary files /dev/null and b/.preview/marwie: everywhere action emphasize on click.png differ
diff --git a/.preview/marwie: usdz hide object on start.png b/.preview/marwie: usdz hide object on start.png
new file mode 100644
index 000000000..5705f7d27
Binary files /dev/null and b/.preview/marwie: usdz hide object on start.png differ
diff --git a/.preview/mercedes benz showcase.png b/.preview/mercedes benz showcase.png
new file mode 100644
index 000000000..426060718
Binary files /dev/null and b/.preview/mercedes benz showcase.png differ
diff --git a/.preview/meta test.png b/.preview/meta test.png
new file mode 100644
index 000000000..dff6fd160
Binary files /dev/null and b/.preview/meta test.png differ
diff --git a/.preview/modules and packages.png b/.preview/modules and packages.png
new file mode 100644
index 000000000..ad31a512d
Binary files /dev/null and b/.preview/modules and packages.png differ
diff --git a/.preview/monster hands.png b/.preview/monster hands.png
new file mode 100644
index 000000000..84746fd5b
Binary files /dev/null and b/.preview/monster hands.png differ
diff --git a/.preview/needle config.png b/.preview/needle config.png
new file mode 100644
index 000000000..deb1022a1
Binary files /dev/null and b/.preview/needle config.png differ
diff --git a/.preview/needle engine attributes.png b/.preview/needle engine attributes.png
new file mode 100644
index 000000000..94d233403
Binary files /dev/null and b/.preview/needle engine attributes.png differ
diff --git a/.preview/needle engine for blender.png b/.preview/needle engine for blender.png
new file mode 100644
index 000000000..733207b98
Binary files /dev/null and b/.preview/needle engine for blender.png differ
diff --git a/.preview/needle engine scripting.png b/.preview/needle engine scripting.png
new file mode 100644
index 000000000..6c0fccb81
Binary files /dev/null and b/.preview/needle engine scripting.png differ
diff --git a/.preview/needle engine.png b/.preview/needle engine.png
new file mode 100644
index 000000000..ce82777d6
Binary files /dev/null and b/.preview/needle engine.png differ
diff --git a/.preview/networking.png b/.preview/networking.png
new file mode 100644
index 000000000..b9a14da06
Binary files /dev/null and b/.preview/networking.png differ
diff --git a/.preview/questions and answers.png b/.preview/questions and answers.png
new file mode 100644
index 000000000..70f5a2556
Binary files /dev/null and b/.preview/questions and answers.png differ
diff --git a/.preview/robyer1: ar move scale rotate controls for needle on mobile.png b/.preview/robyer1: ar move scale rotate controls for needle on mobile.png
new file mode 100644
index 000000000..0633a549d
Binary files /dev/null and b/.preview/robyer1: ar move scale rotate controls for needle on mobile.png differ
diff --git a/.preview/samples projects.png b/.preview/samples projects.png
new file mode 100644
index 000000000..5c97d2ebc
Binary files /dev/null and b/.preview/samples projects.png differ
diff --git a/.preview/scripting examples.png b/.preview/scripting examples.png
new file mode 100644
index 000000000..eebc0d4f1
Binary files /dev/null and b/.preview/scripting examples.png differ
diff --git a/.preview/scripting introduction.png b/.preview/scripting introduction.png
new file mode 100644
index 000000000..8cf39b8f8
Binary files /dev/null and b/.preview/scripting introduction.png differ
diff --git a/.preview/summary.png b/.preview/summary.png
new file mode 100644
index 000000000..e72bfe407
Binary files /dev/null and b/.preview/summary.png differ
diff --git a/.preview/technical overview.png b/.preview/technical overview.png
new file mode 100644
index 000000000..d4827a3a4
Binary files /dev/null and b/.preview/technical overview.png differ
diff --git a/.preview/testimonials.png b/.preview/testimonials.png
new file mode 100644
index 000000000..1d1d3662e
Binary files /dev/null and b/.preview/testimonials.png differ
diff --git a/.preview/testing on local devices.png b/.preview/testing on local devices.png
new file mode 100644
index 000000000..b922006c9
Binary files /dev/null and b/.preview/testing on local devices.png differ
diff --git a/.preview/tower defense.png b/.preview/tower defense.png
new file mode 100644
index 000000000..f68125071
Binary files /dev/null and b/.preview/tower defense.png differ
diff --git a/.preview/typescript decorators.png b/.preview/typescript decorators.png
new file mode 100644
index 000000000..39934e474
Binary files /dev/null and b/.preview/typescript decorators.png differ
diff --git a/.preview/typescript essentials.png b/.preview/typescript essentials.png
new file mode 100644
index 000000000..4b2b1737c
Binary files /dev/null and b/.preview/typescript essentials.png differ
diff --git a/.preview/vanilla javascript.png b/.preview/vanilla javascript.png
new file mode 100644
index 000000000..63b8136e8
Binary files /dev/null and b/.preview/vanilla javascript.png differ
diff --git a/.preview/vision.png b/.preview/vision.png
new file mode 100644
index 000000000..668d664a8
Binary files /dev/null and b/.preview/vision.png differ
diff --git a/.preview/vr & ar.png b/.preview/vr & ar.png
new file mode 100644
index 000000000..d2e4ecf38
Binary files /dev/null and b/.preview/vr & ar.png differ
diff --git a/.preview/web3kev: network instantiation of multiple objects.png b/.preview/web3kev: network instantiation of multiple objects.png
new file mode 100644
index 000000000..e18a5f413
Binary files /dev/null and b/.preview/web3kev: network instantiation of multiple objects.png differ
diff --git a/.preview/web3kev: squeeze to scale object or world in vr.png b/.preview/web3kev: squeeze to scale object or world in vr.png
new file mode 100644
index 000000000..597e29cab
Binary files /dev/null and b/.preview/web3kev: squeeze to scale object or world in vr.png differ
diff --git a/.preview/web3kev: vertical move in vr using the right joystick quest.png b/.preview/web3kev: vertical move in vr using the right joystick quest.png
new file mode 100644
index 000000000..fddf9dfc2
Binary files /dev/null and b/.preview/web3kev: vertical move in vr using the right joystick quest.png differ
diff --git a/404.html b/404.html
new file mode 100644
index 000000000..bd0202cfb
--- /dev/null
+++ b/404.html
@@ -0,0 +1,33 @@
+
+
+
Generated Projects can either be added to source control or kept dynamic. Adding them to source control unlocks being able to adjust HTML, CSS, etc very flexible. To generate dynamic projects, change their path to ../Library/MyScene. They will be regenerated if needed.
Please follow the instructions in the Authentication section if this is your first time accessing packages by needle on this machine.
Make sure you have a Needle Engine and Exporter license, otherwise the following steps will fail (you'll not be able to get authenticated package access).
Needs to be setup once per machine.
',15),h=e("li",null,[t("Clone this repository and open "),e("code",null,"starter/Authenticate"),t(" with Unity 2020.3.x")],-1),u={href:"https://packages.needle.tools",target:"_blank",rel:"noopener noreferrer"},p={href:"https://packages.needle.tools",target:"_blank",rel:"noopener noreferrer"},g=e("code",null,"i",-1),f=e("code",null,"Registry Info",-1),m=e("li",null,[t("Copy the line containing "),e("code",null,"_authToken"),t(" (see the video below)"),e("br"),e("video",{src:"https://user-images.githubusercontent.com/5083203/166433857-a0c9e29f-9413-4e10-a1a1-2029e3d3ab06.mp4",autoplay:""})],-1),b=e("li",null,"Focus Unity - a notification window should open that the information has been added successfully from your clipboard.",-1),y=e("li",null,"Click save and close Unity. You should now have access rights to the needle package registry.",-1);function _(k,x){const n=a("ExternalLinkIcon");return r(),c("div",null,[d,e("ol",null,[h,e("li",null,[t("Open "),e("a",u,[t("https://packages.needle.tools โก"),o(n)]),t(" in your browser and login (top right corner) with your github account.")]),e("li",null,[t("Return to "),e("a",p,[t("packages.needle.tools โก"),o(n)]),t(" and click the "),g,t(" icon in the top right corner opening the "),f,t(" window.")]),m,b,y])])}const v=i(l,[["render",_],["__file","backlog.html.vue"]]);export{v as default};
diff --git a/assets/buttons.esm-48f94bc9.js b/assets/buttons.esm-48f94bc9.js
new file mode 100644
index 000000000..2069979ae
--- /dev/null
+++ b/assets/buttons.esm-48f94bc9.js
@@ -0,0 +1,5 @@
+/*!
+ * github-buttons v2.27.0
+ * (c) 2023 ใชใคใ
+ * @license BSD-2-Clause
+ */var M=window.document,v=window.Math,k=window.HTMLElement,p=window.XMLHttpRequest,F=function(e,o){for(var t=0,r=e.length;t'}}},download:{heights:{16:{width:16,path:''}}},eye:{heights:{16:{width:16,path:''}}},heart:{heights:{16:{width:16,path:''}}},"issue-opened":{heights:{16:{width:16,path:''}}},"mark-github":{heights:{16:{width:16,path:''}}},package:{heights:{16:{width:16,path:''}}},play:{heights:{16:{width:16,path:''}}},"repo-forked":{heights:{16:{width:16,path:''}}},"repo-template":{heights:{16:{width:16,path:''}}},star:{heights:{16:{width:16,path:''}}}},re=function(e,o){e=Z(e).replace(/^octicon-/,""),m(x,e)||(e="mark-github");var t=o>=24&&24 in x[e].heights?24:16,r=x[e].heights[t];return'"},y={},te=function(e,o){var t=y[e]||(y[e]=[]);if(!(t.push(o)>1)){var r=V(function(){for(delete y[e];o=t.shift();)o.apply(null,arguments)});if(H){var a=new p;f(a,"abort",r),f(a,"error",r),f(a,"load",function(){var d;try{d=JSON.parse(this.responseText)}catch(s){r(s);return}r(this.status!==200,d)}),a.open("GET",e),a.send()}else{var n=this||window;n._=function(d){n._=null,r(d.meta.status!==200,d.data)};var c=A(n.document)("script",{async:!0,src:e+(e.indexOf("?")!==-1?"&":"?")+"callback=_"}),l=function(){n._&&n._({meta:{}})};f(c,"load",l),f(c,"error",l),$(c,/de|m/,l),n.document.getElementsByTagName("head")[0].appendChild(c)}}},T=function(e,o,t){var r=A(e.ownerDocument),a=e.appendChild(r("style",{type:"text/css"})),n=Q+oe(o["data-color-scheme"]);a.styleSheet?a.styleSheet.cssText=n:a.appendChild(e.ownerDocument.createTextNode(n));var c=Z(o["data-size"])==="large",l=r("a",{className:"btn",href:o.href,rel:"noopener",target:"_blank",title:o.title||void 0,"aria-label":o["aria-label"]||void 0,innerHTML:re(o["data-icon"],c?16:14)+" "},[r("span",{},[o["data-text"]||""])]),d=e.appendChild(r("div",{className:"widget"+(c?" widget-lg":"")},[l])),s=l.hostname.replace(/\.$/,"");if(("."+s).substring(s.length-u.length)!=="."+u){l.removeAttribute("href"),t(d);return}var i=(" /"+l.pathname).split(/\/+/);if(((s===u||s==="gist."+u)&&i[3]==="archive"||s===u&&i[3]==="releases"&&(i[4]==="download"||i[4]==="latest"&&i[5]==="download")||s==="codeload."+u)&&(l.target="_top"),Z(o["data-show-count"])!=="true"||s!==u||i[1]==="marketplace"||i[1]==="sponsors"||i[1]==="orgs"||i[1]==="users"||i[1]==="-"){t(d);return}var b,h;if(!i[2]&&i[1])h="followers",b="?tab=followers";else if(!i[3]&&i[2])h="stargazers_count",b="/stargazers";else if(!i[4]&&i[3]==="subscription")h="subscribers_count",b="/watchers";else if(!i[4]&&i[3]==="fork")h="forks_count",b="/forks";else if(i[3]==="issues")h="open_issues_count",b="/issues";else{t(d);return}var S=i[2]?"/repos/"+i[1]+"/"+i[2]:"/users/"+i[1];te.call(this,P+S,function(B,L){if(!B){var w=L[h];d.appendChild(r("a",{className:"social-count",href:L.html_url+b,rel:"noopener",target:"_blank","aria-label":w+" "+h.replace(/_count$/,"").replace("_"," ").slice(0,w<2?-1:void 0)+" on GitHub"},[(""+w).replace(/\B(?=(\d{3})+(?!\d))/g,",")]))}t(d)})},C=window.devicePixelRatio||1,z=function(e){return(C>1?v.ceil(v.round(e*C)/C*2)/2:v.ceil(e))||0},ae=function(e){var o=e.offsetWidth,t=e.offsetHeight;if(e.getBoundingClientRect){var r=e.getBoundingClientRect();o=v.max(o,z(r.width)),t=v.max(t,z(r.height))}return[o,t]},D=function(e,o){e.style.width=o[0]+"px",e.style.height=o[1]+"px"},ne=function(e,o){if(!(e==null||o==null))if(e.getAttribute&&(e=J(e)),R){var t=_("span");T(t.attachShadow({mode:"closed"}),e,function(){o(t)})}else{var r=_("iframe",{src:"javascript:0",title:e.title||void 0,allowtransparency:!0,scrolling:"no",frameBorder:0});D(r,[0,0]),r.style.border="none";var a=function(){var n=r.contentWindow,c;try{c=n.document.body}catch{M.body.appendChild(r.parentNode.removeChild(r));return}E(r,"load",a),T.call(n,c,e,function(l){var d=ae(l);r.parentNode.removeChild(r),I(r,"load",function(){D(r,d)}),r.src=N+"#"+(r.name=W(e)),o(r)})};f(r,"load",a),M.body.appendChild(r)}};export{ne as render};
diff --git a/assets/component-compiler.html-1caac4c2.js b/assets/component-compiler.html-1caac4c2.js
new file mode 100644
index 000000000..ff7062db8
--- /dev/null
+++ b/assets/component-compiler.html-1caac4c2.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-5572989e","path":"/component-compiler.html","title":"Editor Component Generator","lang":"en-US","frontmatter":{"title":"Editor Component Generator","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/editor component generator.png"}],["meta",{"name":"og:description","content":"---\\nWhen working in Unity or Blender then you will notice that when you create a new Needle Engine component in Typescript or Javascript it will automatically generate a Unity Cstub component OR a Blender panel for you.\\nThis is thanks to the magic of the Needle component compiler that runs behind the scenes in an editor environment and watches changes to your script files. When it notices that you created a new Needle Engine component it will then generate the correct Unity component or Blender panel including public variables or properties that you can then set or link from within the Editor.\\nYou can use the following comments in your typescript code to control Ccode generation behavior:"}]],"description":"---\\nWhen working in Unity or Blender then you will notice that when you create a new Needle Engine component in Typescript or Javascript it will automatically generate a Unity Cstub component OR a Blender panel for you.\\nThis is thanks to the magic of the Needle component compiler that runs behind the scenes in an editor environment and watches changes to your script files. When it notices that you created a new Needle Engine component it will then generate the correct Unity component or Blender panel including public variables or properties that you can then set or link from within the Editor.\\nYou can use the following comments in your typescript code to control Ccode generation behavior:"},"headers":[{"level":3,"title":"Automatically generating Editor components","slug":"automatically-generating-editor-components","link":"#automatically-generating-editor-components","children":[]},{"level":3,"title":"Controlling component generation","slug":"controlling-component-generation","link":"#controlling-component-generation","children":[]},{"level":3,"title":"Component Compiler in Unity","slug":"component-compiler-in-unity","link":"#component-compiler-in-unity","children":[]},{"level":3,"title":"Extending generated components","slug":"extending-generated-components","link":"#extending-generated-components","children":[]}],"git":{},"filePathRelative":"component-compiler.md"}');export{e as data};
diff --git a/assets/component-compiler.html-5daafa9f.js b/assets/component-compiler.html-5daafa9f.js
new file mode 100644
index 000000000..7304ef2a5
--- /dev/null
+++ b/assets/component-compiler.html-5daafa9f.js
@@ -0,0 +1,99 @@
+import{_ as i,M as o,p,q as r,R as n,t as s,N as e,V as a,a1 as u}from"./framework-c782e227.js";const d={},k=n("h3",{id:"automatically-generating-editor-components",tabindex:"-1"},[n("a",{class:"header-anchor",href:"automatically-generating-editor-components","aria-hidden":"true"},"#"),s(" Automatically generating Editor components")],-1),m=n("p",null,"When working in Unity or Blender then you will notice that when you create a new Needle Engine component in Typescript or Javascript it will automatically generate a Unity C# stub component OR a Blender panel for you.",-1),b={href:"https://www.npmjs.com/package/@needle-tools/needle-component-compiler",target:"_blank",rel:"noopener noreferrer"},y=u(`
If you want to add scripts inside the src/scripts folder in your project then you need to have a Component Generator on the GameObject with your ExportInfo component. Now when adding new components in your/threejs/project/src/scriptsit will automatically generate Unity scripts in Assets/Needle/Components.codegen. If you want to add scripts to any NpmDef file you can just create them - each NpmDef automatically watches script changes and handles component generation, so you don't need any additional component in your scene.
For C# fields to be correctly generated it is currently important that you explictly declare a Typescript type. For example myField : number = 5
You can switch between Typescript input and generated C# stub components using the tabs below
`,12),v=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" AssetReference"),n("span",{class:"token punctuation"},","),s(" Behaviour"),n("span",{class:"token punctuation"},","),s(" serializable "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Object3D "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"three"'),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"MyCustomComponent"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ myFloatValue`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token builtin"},"number"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"42"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),s("Object3D"),n("span",{class:"token punctuation"},")"),s(`
+ myOtherObject`),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" Object3D"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),s("AssetReference"),n("span",{class:"token punctuation"},")"),s(`
+ prefabs`),n("span",{class:"token operator"},":"),s(" AssetReference"),n("span",{class:"token punctuation"},"["),n("span",{class:"token punctuation"},"]"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"["),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token function"},"start"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"sayHello"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(),n("span",{class:"token function"},"sayHello"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"Hello World"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),h=n("div",{class:"language-csharp line-numbers-mode","data-ext":"cs"},[n("pre",{class:"language-csharp"},[n("code",null,[n("span",{class:"token comment"},"// NEEDLE_CODEGEN_START"),s(`
+`),n("span",{class:"token comment"},"// auto generated code - do not edit directly"),s(`
+
+`),n("span",{class:"token preprocessor property"},[s("#"),n("span",{class:"token directive keyword"},"pragma"),s(" warning disable")]),s(`
+
+`),n("span",{class:"token keyword"},"namespace"),s(),n("span",{class:"token namespace"},[s("Needle"),n("span",{class:"token punctuation"},"."),s("Typescript"),n("span",{class:"token punctuation"},"."),s("GeneratedComponents")]),s(`
+`),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token keyword"},"partial"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"MyCustomComponent"),s(),n("span",{class:"token punctuation"},":"),s(),n("span",{class:"token type-list"},[n("span",{class:"token class-name"},[s("UnityEngine"),n("span",{class:"token punctuation"},"."),s("MonoBehaviour")])]),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token class-name"},[n("span",{class:"token keyword"},"float")]),s(" @myFloatValue "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"42f"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token class-name"},[s("UnityEngine"),n("span",{class:"token punctuation"},"."),s("Transform")]),s(" @myOtherObject"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token class-name"},[s("UnityEngine"),n("span",{class:"token punctuation"},"."),s("Transform"),n("span",{class:"token punctuation"},"["),n("span",{class:"token punctuation"},"]")]),s(" @prefabs "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token constructor-invocation class-name"},[s("UnityEngine"),n("span",{class:"token punctuation"},"."),s("Transform"),n("span",{class:"token punctuation"},"["),n("span",{class:"token punctuation"},"]")]),n("span",{class:"token punctuation"},"{"),s(),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token return-type class-name"},[n("span",{class:"token keyword"},"void")]),s(),n("span",{class:"token function"},"start"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"{"),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token return-type class-name"},[n("span",{class:"token keyword"},"void")]),s(),n("span",{class:"token function"},"update"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"{"),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+
+`),n("span",{class:"token comment"},"// NEEDLE_CODEGEN_END"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),g=n("div",{class:"language-csharp line-numbers-mode","data-ext":"cs"},[n("pre",{class:"language-csharp"},[n("code",null,[n("span",{class:"token keyword"},"using"),s(),n("span",{class:"token namespace"},"UnityEditor"),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token comment"},"// you can add code above or below the NEEDLE_CODEGEN_ blocks"),s(`
+
+`),n("span",{class:"token comment"},"// NEEDLE_CODEGEN_START"),s(`
+`),n("span",{class:"token comment"},"// auto generated code - do not edit directly"),s(`
+
+`),n("span",{class:"token preprocessor property"},[s("#"),n("span",{class:"token directive keyword"},"pragma"),s(" warning disable")]),s(`
+
+`),n("span",{class:"token keyword"},"namespace"),s(),n("span",{class:"token namespace"},[s("Needle"),n("span",{class:"token punctuation"},"."),s("Typescript"),n("span",{class:"token punctuation"},"."),s("GeneratedComponents")]),s(`
+`),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token keyword"},"partial"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"MyCustomComponent"),s(),n("span",{class:"token punctuation"},":"),s(),n("span",{class:"token type-list"},[n("span",{class:"token class-name"},[s("UnityEngine"),n("span",{class:"token punctuation"},"."),s("MonoBehaviour")])]),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token class-name"},[n("span",{class:"token keyword"},"float")]),s(" @myFloatValue "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"42f"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token class-name"},[s("UnityEngine"),n("span",{class:"token punctuation"},"."),s("Transform")]),s(" @myOtherObject"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token class-name"},[s("UnityEngine"),n("span",{class:"token punctuation"},"."),s("Transform"),n("span",{class:"token punctuation"},"["),n("span",{class:"token punctuation"},"]")]),s(" @prefabs "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token constructor-invocation class-name"},[s("UnityEngine"),n("span",{class:"token punctuation"},"."),s("Transform"),n("span",{class:"token punctuation"},"["),n("span",{class:"token punctuation"},"]")]),n("span",{class:"token punctuation"},"{"),s(),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token return-type class-name"},[n("span",{class:"token keyword"},"void")]),s(),n("span",{class:"token function"},"start"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"{"),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token return-type class-name"},[n("span",{class:"token keyword"},"void")]),s(),n("span",{class:"token function"},"update"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"{"),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+
+`),n("span",{class:"token comment"},"// NEEDLE_CODEGEN_END"),s(`
+
+`),n("span",{class:"token keyword"},"namespace"),s(),n("span",{class:"token namespace"},[s("Needle"),n("span",{class:"token punctuation"},"."),s("Typescript"),n("span",{class:"token punctuation"},"."),s("GeneratedComponents")]),s(`
+`),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"// This is how you extend the generated component (namespace and class name must match!)"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token keyword"},"partial"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"MyCustomComponent"),s(),n("span",{class:"token punctuation"},":"),s(),n("span",{class:"token type-list"},[n("span",{class:"token class-name"},[s("UnityEngine"),n("span",{class:"token punctuation"},"."),s("MonoBehaviour")])]),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token return-type class-name"},[n("span",{class:"token keyword"},"void")]),s(),n("span",{class:"token function"},"MyAdditionalMethod"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(),n("span",{class:"token return-type class-name"},[n("span",{class:"token keyword"},"void")]),s(),n("span",{class:"token function"},"OnValidate"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ myFloatValue `),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"42"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token comment"},"// of course you can also add custom editors"),s(`
+ `),n("span",{class:"token punctuation"},"["),n("span",{class:"token function"},"CustomEditor"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"typeof"),n("span",{class:"token punctuation"},"("),n("span",{class:"token type-expression class-name"},"MyCustomComponent"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"]"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"MyCustomComponentEditor"),s(),n("span",{class:"token punctuation"},":"),s(),n("span",{class:"token type-list"},[n("span",{class:"token class-name"},"Editor")]),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token keyword"},"override"),s(),n("span",{class:"token return-type class-name"},[n("span",{class:"token keyword"},"void")]),s(),n("span",{class:"token function"},"OnInspectorGUI"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ EditorGUILayout`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"HelpBox"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"This is my sample component"'),n("span",{class:"token punctuation"},","),s(" MessageType"),n("span",{class:"token punctuation"},"."),s("None"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"base"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"OnInspectorGUI"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),w=n("h3",{id:"extending-generated-components",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#extending-generated-components","aria-hidden":"true"},"#"),s(" Extending generated components")],-1),f={href:"https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods",target:"_blank",rel:"noopener noreferrer"},C=n("code",null,"partial",-1),_=n("div",{class:"custom-container tip"},[n("p",{class:"custom-container-title"},"Member Casing"),n("p",null,[s("Exported members will start with a lowercase letter. For example if your C# member is named "),n("code",null,"MyString"),s(" it will be assigned to "),n("code",null,"myString"),s(".")])],-1);function E(x,N){const c=o("ExternalLinkIcon"),t=o("CodeGroupItem"),l=o("CodeGroup");return p(),r("div",null,[k,m,n("p",null,[s("This is thanks to the magic of the "),n("a",b,[s("Needle component compiler"),e(c)]),s(" that runs behind the scenes in an editor environment and watches changes to your script files. When it notices that you created a new Needle Engine component it will then generate the correct Unity component or Blender panel including public variables or properties that you can then set or link from within the Editor.")]),y,e(l,null,{default:a(()=>[e(t,{title:"Typescript"},{default:a(()=>[v]),_:1}),e(t,{title:"Generated C#"},{default:a(()=>[h]),_:1}),e(t,{title:"Extending Generated C#"},{default:a(()=>[g]),_:1})]),_:1}),w,n("p",null,[s("Component C# classes are generated with the "),n("a",f,[C,e(c)]),s(" flag so that it is easy to extend them with functionality. This is helpful to draw gizmos, add context menus or add additional fields or methods that are not part of a built-in component.")]),_])}const M=i(d,[["render",E],["__file","component-compiler.html.vue"]]);export{M as default};
diff --git a/assets/component-reference.html-458a6aba.js b/assets/component-reference.html-458a6aba.js
new file mode 100644
index 000000000..e73e18900
--- /dev/null
+++ b/assets/component-reference.html-458a6aba.js
@@ -0,0 +1 @@
+import{_ as s,M as i,p as c,q as l,R as t,t as e,N as o,V as a,a1 as d}from"./framework-c782e227.js";const h={},p=t("p",null,"Here is a overview of some of the components that we provide. Some of them map directly to Unity components, while others are core components from Needle Engine.",-1),u=t("p",null,[e("For a complete list please have a look at the components inside the folders "),t("code",null,"node_modules/@needle-tools/engine/engine-components"),e(" and "),t("code",null,"engine-components-experimental"),e(".")],-1),m=t("p",null,"You can always add your own components or add wrappers for Unity components we haven't provided yet.",-1),b=d('
DirectionalLight, PointLight, Spotlight. Note that you can use it to bake light (e.g. Rectangular Light shapes) as well
XRFlag
Control when objects will be visible. E.g. only enable object when in AR
DeviceFlag
Control on which device objects will be visible
LODGroup
ParticleSystem
Experimental and currently not fully supported
VideoPlayer
Playback videos from url or referenced video file (will be copied to output on export). The VideoPlayer also supports streaming from MediaStream objects or M3U8 livestream URLs
MeshRenderer
Used to handle rendering of objects including lightmapping and instancing
Unity only: Note that Postprocessing effect export in Unity is only supported with URP.
Unity only: extra Component means that the effect component is an extra component that has to be added next to the Volume component. For example for Antialiasing add a Volume component and an Antialiasing component to the same GameObject.
Effect Name
Antialiasing
extra Unity Component
Bloom
via Volume asset
Chromatic Aberration
via Volume asset
Color Adjustments / Color Correction
via Volume asset
Depth Of Field
via Volume asset
Pixelation
Screenspace Ambient Occlusion N8
Screenspace Ambient Occlusion
Tilt Shift Effect
Vignette
via Volume asset
Your custom effect
Create a new class that extends from Needle Engine's PostProcessingEffect class. Then call registerCustomEffectType with your effect name and class type.
',2),_=t("thead",null,[t("tr",null,[t("th",null,"Name"),t("th",null,"Description")])],-1),x=t("tr",null,[t("td",null,[t("code",null,"WebXR")]),t("td",null,"Add to scene for VR, AR and Passthrough support as well as rendering Avatar models")],-1),R=t("code",null,"USDZExporter",-1),v=t("td",null,"Add to enable USD and Quicklook support",-1),k=t("tr",null,[t("td",null,[t("code",null,"XRFlag")]),t("td",null,"Control when objects are visible, e.g. only in VR or AR or only in ThirdPerson")],-1),C=t("tr",null,[t("td",null,[t("code",null,"WebARSessionRoot")]),t("td",null,"Handles placement and scale of your scene in AR mode")],-1),A=t("tr",null,[t("td",null,[t("code",null,"WebARCameraBackground")]),t("td",null,"Add to access the AR camera image and apply effects or use it for rendering")],-1),D=t("tr",null,[t("td",null,[t("code",null,"WebXRImageTracking")]),t("td",null,"Assign images to be tracked and optionally instantiate an object at the image position")],-1),S=t("tr",null,[t("td",null,[t("code",null,"WebXRPlaneTracking")]),t("td",null,"Create plane meshes or colliders for tracked planes")],-1),U=t("tr",null,[t("td",null,[t("code",null,"XRControllerModel")]),t("td",null,"Can be added to render device controllers or hand models (will be created by default when enabled in the WebXR component)")],-1),j=t("tr",null,[t("td",null,[t("code",null,"XRControllerMovement")]),t("td",null,"Can be added to provide default movement and teleport controls")],-1),N=t("tr",null,[t("td",null,[t("code",null,"XRControllerFollow")]),t("td",null,"Can be added to any object in the scene and configured to follow either left or right hands or controllers")],-1),P=d('
Unity's UI system. Needs to be in World Space mode right now.
Text
Render Text using Unity's UI. Custom fonts are supported, a font atlas will be automatically generated on export. Use the font settings to control which characters are included in the atlas
Button
Receives click events - use the onClick event to react to it. It can be added too 3D scene objects as well
Image
Renders a sprite image
RawImage
Renders a texture
Note: Depending on your project, often a mix of spatial and 2D UI makes sense for cross-platform projects where VR, AR, and screens are supported. Typically, you'd build the 2D parts with HTML for best accessibility, and the 3D parts with geometric UIs that also support depth offsets (e.g. button hover states and the like).
Handles loading and unloading of other scenes or prefabs / glTF files. Has features to preload, change scenes via swiping, keyboard events or URL navigation
Main component for managing the web project(s) to e.g. install or start the web app
EditorSync
Add to enable networking material or component value changes to the running three.js app directly from the Unity Editor without having to reload
',6);function T(V,L){const r=i("RouterLink"),n=i("ExternalLinkIcon");return c(),l("div",null,[p,u,m,t("p",null,[e("Learn more in the "),o(r,{to:"/scripting.html"},{default:a(()=>[e("Scripting")]),_:1}),e(" section of our docs.")]),b,t("p",null,[e("Postprocessing effects use the "),t("a",g,[e("pmndrs postprocessing library"),o(n)]),e(" under the hood. This means you can also easily add your own custom effects and get an automatically optimized postprocessing pass.")]),f,t("p",null,[e("Physics is implemented using "),t("a",y,[e("Rapier"),o(n)]),e(".")]),w,t("p",null,[o(r,{to:"/xr.html"},{default:a(()=>[e("Read the XR docs")]),_:1})]),t("table",null,[_,t("tbody",null,[x,t("tr",null,[t("td",null,[o(r,{to:"/everywhere-actions.html"},{default:a(()=>[R]),_:1})]),v]),k,C,A,D,S,U,j,N])]),P,t("p",null,[e("Spatial UI components are mapped from Unity UI (Canvas, not UI Toolkit) to "),t("a",E,[e("three-mesh-ui"),o(n)]),e(". UI can be animated.")]),I])}const X=s(h,[["render",T],["__file","component-reference.html.vue"]]);export{X as default};
diff --git a/assets/component-reference.html-7aacb907.js b/assets/component-reference.html-7aacb907.js
new file mode 100644
index 000000000..888c7ba22
--- /dev/null
+++ b/assets/component-reference.html-7aacb907.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-d8eff192","path":"/component-reference.html","title":"Core Components","lang":"en-US","frontmatter":{"title":"Core Components","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/core components.png"}],["meta",{"name":"og:description","content":"---\\nHere is a overview of some of the components that we provide. Some of them map directly to Unity components, while others are core components from Needle Engine.\\nFor a complete list please have a look at the components inside the folders node_modules/@needle-tools/engine/engine-components and engine-components-experimental.\\nYou can always add your own components or add wrappers for Unity components we haven't provided yet.\\nLearn more in the Scripting section of our docs."}]],"description":"---\\nHere is a overview of some of the components that we provide. Some of them map directly to Unity components, while others are core components from Needle Engine.\\nFor a complete list please have a look at the components inside the folders node_modules/@needle-tools/engine/engine-components and engine-components-experimental.\\nYou can always add your own components or add wrappers for Unity components we haven't provided yet.\\nLearn more in the Scripting section of our docs."},"headers":[{"level":2,"title":"Audio","slug":"audio","link":"#audio","children":[]},{"level":2,"title":"Animation","slug":"animation","link":"#animation","children":[]},{"level":2,"title":"Rendering","slug":"rendering","link":"#rendering","children":[{"level":3,"title":"Postprocessing","slug":"postprocessing","link":"#postprocessing","children":[]}]},{"level":2,"title":"Networking","slug":"networking","link":"#networking","children":[]},{"level":2,"title":"Interaction","slug":"interaction","link":"#interaction","children":[]},{"level":2,"title":"Physics","slug":"physics","link":"#physics","children":[]},{"level":2,"title":"XR / WebXR","slug":"xr-webxr","link":"#xr-webxr","children":[]},{"level":2,"title":"Debugging","slug":"debugging","link":"#debugging","children":[]},{"level":2,"title":"Runtime File Input/Output","slug":"runtime-file-input-output","link":"#runtime-file-input-output","children":[]},{"level":2,"title":"UI","slug":"ui","link":"#ui","children":[]},{"level":2,"title":"Other","slug":"other","link":"#other","children":[]},{"level":2,"title":"Editor Only","slug":"editor-only","link":"#editor-only","children":[]}],"git":{},"filePathRelative":"component-reference.md"}`);export{e as data};
diff --git a/assets/contribution-header-ed879e44.js b/assets/contribution-header-ed879e44.js
new file mode 100644
index 000000000..e4fa21f51
--- /dev/null
+++ b/assets/contribution-header-ed879e44.js
@@ -0,0 +1 @@
+import{_ as s,p as i,q as n,R as t,v as r,Q as a,w as l,s as c}from"./framework-c782e227.js";const d={class:"contribution"},u={class:"profile"},h=["src"],_=["href"],g={class:"links"},f=["href"],m={key:0,class:"title"},v={__name:"contribution-header",props:{title:String,url:String,page:String,author:String,profileImage:String,githubUrl:String,gradient:Boolean},setup(e){return(o,b)=>(i(),n("div",d,[t("div",{class:l(["header",e.gradient?"gradient":""])},[t("div",u,[t("img",{src:e.profileImage,alt:"profile image"},null,8,h),t("a",{class:"authorname",href:e.page},[t("span",null,r(e.author),1)],8,_)]),t("div",g,[e.githubUrl?(i(),n("a",{key:0,href:e.githubUrl,target:"_blank",rel:"noopener noreferrer"},"View on Github",8,f)):a("v-if",!0)])],2),e.title?(i(),n("div",m,[t("h2",null,r(e.title),1)])):a("v-if",!0),c(o.$slots,"default",{},void 0,!0)]))}},k=s(v,[["__scopeId","data-v-543822ed"],["__file","contribution-header.vue"]]);export{k as default};
diff --git a/assets/contribution-listentry-32167b7e.js b/assets/contribution-listentry-32167b7e.js
new file mode 100644
index 000000000..2f5424042
--- /dev/null
+++ b/assets/contribution-listentry-32167b7e.js
@@ -0,0 +1 @@
+import{_ as e,p as n,q as r,R as s,v as c}from"./framework-c782e227.js";const i=["href"],o={__name:"contribution-listentry",props:{title:String,url:String},setup(t){return(_,a)=>(n(),r("a",{class:"entry",href:t.url},[s("div",null,c(t.title),1)],8,i))}},u=e(o,[["__scopeId","data-v-bcc3d6f6"],["__file","contribution-listentry.vue"]]);export{u as default};
diff --git a/assets/contribution-preview-edd29e75.js b/assets/contribution-preview-edd29e75.js
new file mode 100644
index 000000000..a1989c810
--- /dev/null
+++ b/assets/contribution-preview-edd29e75.js
@@ -0,0 +1 @@
+import{_ as s,p as c,q as r,R as t,v as a,s as _}from"./framework-c782e227.js";const l={class:"title"},p={class:"content"},d={__name:"contribution-preview",props:{title:String,pageUrl:String},setup(e){const o=e;function n(){window.location.href=o.pageUrl}return(i,u)=>(c(),r("div",{class:"preview",onClick:n},[t("div",l,a(e.title),1),t("div",p,[_(i.$slots,"default",{},void 0,!0)])]))}},f=s(d,[["__scopeId","data-v-f801cb6e"],["__file","contribution-preview.vue"]]);export{f as default};
diff --git a/assets/contributions-author-e2ce8cd4.js b/assets/contributions-author-e2ce8cd4.js
new file mode 100644
index 000000000..4aca77267
--- /dev/null
+++ b/assets/contributions-author-e2ce8cd4.js
@@ -0,0 +1 @@
+import{_ as s,M as a,p as o,q as r,Q as u,N as c,R as t,s as l,O as h}from"./framework-c782e227.js";const d=["href"],g={class:"previews"},v=t("div",{class:"footer"},[t("a",{href:"https://github.com/needle-tools/needle-engine-support/discussions/new?category=share"},"Add your contribution")],-1),_={__name:"contributions-author",props:{name:String,url:String,githubUrl:String,profileImage:String,overviewLink:String},setup(e){return(n,m)=>{const i=a("contribution-header");return o(),r(h,null,[e.overviewLink?(o(),r("a",{key:0,href:e.overviewLink,class:"overview-link"},"โ Overview",8,d)):u("v-if",!0),c(i,{profileImage:e.profileImage,author:e.name,githubUrl:e.githubUrl},null,8,["profileImage","author","githubUrl"]),t("div",g,[l(n.$slots,"default")]),v],64)}}},b=s(_,[["__file","contributions-author.vue"]]);export{b as default};
diff --git a/assets/contributions-overview-3702ea45.js b/assets/contributions-overview-3702ea45.js
new file mode 100644
index 000000000..8d6603c45
--- /dev/null
+++ b/assets/contributions-overview-3702ea45.js
@@ -0,0 +1 @@
+import{_ as t,p as s,q as o,s as n}from"./framework-c782e227.js";const r={},_={class:"list"};function c(e,i){return s(),o("div",_,[n(e.$slots,"default")])}const l=t(r,[["render",c],["__file","contributions-overview.vue"]]);export{l as default};
diff --git a/assets/copyright-f1acaab6.js b/assets/copyright-f1acaab6.js
new file mode 100644
index 000000000..776211825
--- /dev/null
+++ b/assets/copyright-f1acaab6.js
@@ -0,0 +1 @@
+import{_ as t,p as o,q as c,t as a,a9 as s,aa as _,R as p}from"./framework-c782e227.js";const r={},d=e=>(s("data-v-c089a4fb"),e=e(),_(),e),i={class:"footer"},n=d(()=>p("a",{target:"_blank",href:"https://needle.tools/contact#privacy-policy"},"Privacy Policy",-1));function l(e,f,h,y,u,v){return o(),c("div",i,[a(" Copyright ยฉ 2022 Needle Tools GmbH โ "),n])}const b=t(r,[["render",l],["__scopeId","data-v-c089a4fb"],["__file","copyright.vue"]]);export{b as default};
diff --git a/assets/custom-loading-style-65a23c0d.js b/assets/custom-loading-style-65a23c0d.js
new file mode 100644
index 000000000..7aab91972
--- /dev/null
+++ b/assets/custom-loading-style-65a23c0d.js
@@ -0,0 +1 @@
+const s="/docs/imgs/custom-loading-style.webp";export{s as _};
diff --git a/assets/debugging.html-21018bdf.js b/assets/debugging.html-21018bdf.js
new file mode 100644
index 000000000..a16d8fa61
--- /dev/null
+++ b/assets/debugging.html-21018bdf.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-f588dc38","path":"/debugging.html","title":"How To Debug","lang":"en-US","frontmatter":{"title":"How To Debug","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/how to debug.png"}],["meta",{"name":"og:description","content":"---\\nTo inspect glTF or glb files online:"}]],"description":"---\\nTo inspect glTF or glb files online:"},"headers":[{"level":2,"title":"Useful resources for working with glTF","slug":"useful-resources-for-working-with-gltf","link":"#useful-resources-for-working-with-gltf","children":[]},{"level":2,"title":"Built-in URL parameters","slug":"built-in-url-parameters","link":"#built-in-url-parameters","children":[]},{"level":2,"title":"Debug Methods","slug":"debug-methods","link":"#debug-methods","children":[]},{"level":2,"title":"Local Testing of release builds","slug":"local-testing-of-release-builds","link":"#local-testing-of-release-builds","children":[]},{"level":2,"title":"VSCode","slug":"vscode","link":"#vscode","children":[]},{"level":2,"title":"Mobile","slug":"mobile","link":"#mobile","children":[{"level":3,"title":"Android Debugging","slug":"android-debugging","link":"#android-debugging","children":[]},{"level":3,"title":"iOS Debugging","slug":"ios-debugging","link":"#ios-debugging","children":[]},{"level":3,"title":"Quest Debugging","slug":"quest-debugging","link":"#quest-debugging","children":[]}]}],"git":{},"filePathRelative":"debugging.md"}');export{e as data};
diff --git a/assets/debugging.html-45ff63db.js b/assets/debugging.html-45ff63db.js
new file mode 100644
index 000000000..c8854d66c
--- /dev/null
+++ b/assets/debugging.html-45ff63db.js
@@ -0,0 +1,13 @@
+import{_ as l,M as a,p as i,q as d,R as e,t as o,N as n,V as c,a1 as s}from"./framework-c782e227.js";const u="/docs/debugging/vscode-start-debugging.webp",p={},h=e("h2",{id:"useful-resources-for-working-with-gltf",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#useful-resources-for-working-with-gltf","aria-hidden":"true"},"#"),o(" Useful resources for working with glTF")],-1),g=e("p",null,"To inspect glTF or glb files online:",-1),b={href:"https://gltf.report/",target:"_blank",rel:"noopener noreferrer"},m={href:"https://modelviewer.dev/editor",target:"_blank",rel:"noopener noreferrer"},_={href:"https://github.khronos.org/glTF-Sample-Viewer-Release/",target:"_blank",rel:"noopener noreferrer"},f={href:"https://sandbox.babylonjs.com/",target:"_blank",rel:"noopener noreferrer"},v={href:"https://github.khronos.org/glTF-Validator/",target:"_blank",rel:"noopener noreferrer"},k=e("p",null,"To inspect them locally:",-1),w={href:"https://apps.microsoft.com/store/detail/gltf-shell-extensions/9NPGVJ9N57MV?hl=en-us&gl=US",target:"_blank",rel:"noopener noreferrer"},y={href:"https://marketplace.visualstudio.com/items?itemName=cesium.gltf-vscode",target:"_blank",rel:"noopener noreferrer"},q=s('
',5),x=e("code",null,"Gizmos",-1),S=e("h2",{id:"local-testing-of-release-builds",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#local-testing-of-release-builds","aria-hidden":"true"},"#"),o(" Local Testing of release builds")],-1),V=s("
If you have changed the port on which your server starts make sure to update the url field accordingly. You can then start your local server from within VSCode:
For Android debugging, you can attach Chrome Dev Tools to your device and see logs right from your PC. You have to switch your device into development mode and connect it via USB.
',7),L={href:"https://developer.chrome.com/docs/devtools/remote-debugging/",target:"_blank",rel:"noopener noreferrer"},B={href:"https://developer.android.com/studio/debug/dev-options",target:"_blank",rel:"noopener noreferrer"},N=e("li",null,"Connect your phone to your computer via USB",-1),U=e("li",null,[o("Open this url in your browser "),e("code",null,"chrome://inspect/#devices")],-1),z=e("li",null,"On your mobile device allow the USB connection to your computer",-1),A=e("li",null,[o("On your computer in chrome you should see a list of open tabs after a while (on "),e("code",null,"chrome://inspect/#devices"),o(")")],-1),D=e("li",null,[o("Click "),e("code",null,"Inspect"),o(" on the tab you want to debug")],-1),M=e("h3",{id:"ios-debugging",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#ios-debugging","aria-hidden":"true"},"#"),o(" iOS Debugging")],-1),E=e("p",null,[o("For easy iOS debugging add the "),e("code",null,"?console"),o(" URL parameter to get a useful on-screen JavaScript console.")],-1),O=e("p",null,"If you have a Mac, you can also attach to Safari (similar to the Android workflow above).",-1),I={href:"https://labs.mozilla.org/projects/webxr-viewer/",target:"_blank",rel:"noopener noreferrer"},W=e("h3",{id:"quest-debugging",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#quest-debugging","aria-hidden":"true"},"#"),o(" Quest Debugging")],-1),X=e("p",null,[o("Quest is just an Android device - see the "),e("a",{href:"#android-debugging"},"Android Debugging"),o(" section for steps.")],-1);function Y(G,P){const t=a("ExternalLinkIcon"),r=a("RouterLink");return i(),d("div",null,[h,g,e("ul",null,[e("li",null,[e("a",b,[o("gltf.report"),n(t)]),o(" - three.js based")]),e("li",null,[e("a",m,[o("modelviewer.dev/editor"),n(t)]),o(" - three.js based")]),e("li",null,[e("a",_,[o("Khronos glTF Sample Viewer"),n(t)])]),e("li",null,[e("a",f,[o("Babylon Sandbox"),n(t)])]),e("li",null,[e("a",v,[o("glTF Validator"),n(t)])])]),k,e("ul",null,[e("li",null,[o("use the "),e("a",w,[o("glTF Shell Extension for Windows"),n(t)]),o(" to convert between glTF and glb")]),e("li",null,[o("use the "),e("a",y,[o("glTF Tools VS Code Extension"),n(t)]),o(" to see validation errors and in-engine previews locally")])]),q,e("p",null,[o("Needle Engine also has some very powerful and useful debugging methods that are part of the static "),x,o(" class. See the "),n(r,{to:"/scripting.html#gizmos"},{default:c(()=>[o("scripting documentation")]),_:1}),o(" for more information.")]),S,e("ul",null,[V,e("li",null,[o("optional: if you want to test WebXR, generate a "),e("a",F,[o("self-signed SSL certificate"),n(t)]),o(", then run "),T,o(" to enable https (required for WebXR).")])]),C,e("p",null,[o("You can attach VSCode to the running local server to set breakpoints and debug your code. You can read more about "),e("a",j,[o("debugging with VSCode"),n(t)]),o(" here.")]),R,e("p",null,[o("See the official chrome documentation "),e("a",L,[o("here"),n(t)])]),e("ul",null,[e("li",null,[o("Make sure "),e("a",B,[o("Developer Mode"),n(t)]),o(" is enabled on your phone")]),N,U,z,A,D]),M,E,O,e("p",null,[o("WebXR usage and debugging on iOS requires using a third-party browser: "),e("a",I,[o("Mozilla WebXR Viewer"),n(t)]),o(".")]),W,X])}const J=l(p,[["render",Y],["__file","debugging.html.vue"]]);export{J as default};
diff --git a/assets/deployment.html-75760b2e.js b/assets/deployment.html-75760b2e.js
new file mode 100644
index 000000000..e512a484b
--- /dev/null
+++ b/assets/deployment.html-75760b2e.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-4d21151b","path":"/deployment.html","title":"Deployment & Compression","lang":"en-US","frontmatter":{"title":"Deployment & Compression","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/deployment & compression.png"}],["meta",{"name":"og:description","content":"---\\nDeployment is the process of making your application available to the public on a website. Needle Engine ensures that your project is as small and fast as possible by using the latest compression techniques such as KTX2, Draco, and Meshopt."}]],"description":"---\\nDeployment is the process of making your application available to the public on a website. Needle Engine ensures that your project is as small and fast as possible by using the latest compression techniques such as KTX2, Draco, and Meshopt."},"headers":[{"level":2,"title":"What does deployment mean?","slug":"what-does-deployment-mean","link":"#what-does-deployment-mean","children":[]},{"level":2,"title":"Available Deployment Targets","slug":"available-deployment-targets","link":"#available-deployment-targets","children":[]},{"level":2,"title":"Deploying to the web from Unity","slug":"deploying-to-the-web-from-unity","link":"#deploying-to-the-web-from-unity","children":[]},{"level":2,"title":"Development Builds","slug":"development-builds","link":"#development-builds","children":[]},{"level":2,"title":"Production Builds","slug":"production-builds","link":"#production-builds","children":[{"level":3,"title":"Optimization and Compression Options","slug":"optimization-and-compression-options","link":"#optimization-and-compression-options","children":[]}]},{"level":2,"title":"Deployment Options","slug":"deployment-options","link":"#deployment-options","children":[{"level":3,"title":"Deploy to Glitch ๐","slug":"deploy-to-glitch","link":"#deploy-to-glitch","children":[]},{"level":3,"title":"Deploy to Netlify","slug":"deploy-to-netlify","link":"#deploy-to-netlify","children":[]},{"level":3,"title":"Deploy to Vercel","slug":"deploy-to-vercel","link":"#deploy-to-vercel","children":[]},{"level":3,"title":"Deploy to itch.io","slug":"deploy-to-itch.io","link":"#deploy-to-itch.io","children":[]},{"level":3,"title":"Deploy to FTP","slug":"deploy-to-ftp","link":"#deploy-to-ftp","children":[]},{"level":3,"title":"Deploy to Github Pages","slug":"deploy-to-github-pages","link":"#deploy-to-github-pages","children":[]},{"level":3,"title":"Deploy to Facebook Instant Games","slug":"deploy-to-facebook-instant-games","link":"#deploy-to-facebook-instant-games","children":[]}]},{"level":2,"title":"Cross-Platform Deployment Workflows","slug":"cross-platform-deployment-workflows","link":"#cross-platform-deployment-workflows","children":[]},{"level":2,"title":"Needle Engine Commandline Arguments for Unity","slug":"needle-engine-commandline-arguments-for-unity","link":"#needle-engine-commandline-arguments-for-unity","children":[]}],"git":{},"filePathRelative":"deployment.md"}');export{e as data};
diff --git a/assets/deployment.html-dfaa9897.js b/assets/deployment.html-dfaa9897.js
new file mode 100644
index 000000000..fcd0305e0
--- /dev/null
+++ b/assets/deployment.html-dfaa9897.js
@@ -0,0 +1 @@
+import{_ as d}from"./texture-compression-8cd31165.js";import{_ as c}from"./ktx-env-variable-d006aea1.js";import{_ as p,M as s,p as h,q as u,R as e,t,N as o,V as m,a1 as n}from"./framework-c782e227.js";const g="/docs/imgs/unity-build-window-menu.jpg",y="/docs/imgs/unity-build-window.jpg",a="/docs/imgs/unity-texture-compression.jpg",b="/docs/imgs/unity-texture-compression-options.jpg",f="/docs/imgs/unity-progressive-textures.jpg",_="/docs/imgs/unity-mesh-compression-component.jpg",w="/docs/imgs/unity-mesh-simplification.jpg",v="/docs/deployment/deploytoglitch-1.jpg",x="/docs/deployment/deploytoglitch-2.jpg",k="/docs/blender/deploy_to_glitch.webp",T="/docs/deployment/deploytonetlify-2.jpg",j="/docs/deployment/deploytonetlify.jpg",P="/docs/deployment/deploytoftp.jpg",D="/docs/deployment/deploytoftp2.jpg",G="/docs/deployment/deploytoftp3.jpg",F="/docs/deployment/buildoptions_gzip.jpg",I="/docs/deployment/deploytogithubpages.jpg",U="/docs/deployment/deploytofacebookinstantgames.jpg",N="/docs/deployment/deploytofacebookinstantgames-hosting.jpg",C="/docs/deployment/deploytofacebookinstantgames-upload.jpg",B="/docs/deployment/facebookinstantgames-1.jpg",S="/docs/deployment/facebookinstantgames-2.jpg",A="/docs/deployment/facebookinstantgames-3.jpg",E={},O=n('
Deployment is the process of making your application available to the public on a website. Needle Engine ensures that your project is as small and fast as possible by using the latest compression techniques such as KTX2, Draco, and Meshopt.
Open File/Build Settings and select Needle Engine for options!
Where do I find the Build Options in Unity?
To build your web project for deployment to a web server you can click Build in the Unity Editor Build Settings Window. You can enable the Development Build checkbox to omit compression (see below) which requires toktx to be installed on your machine.
Nodejs is only required during development. The distributed website (using our default vite template) is a static page that doesn't rely on Nodejs and can be put on any regular web server. Nodejs is required if you want to run our minimalistic networking server on the same web server (automatically contained in the Glitch deployment process).
See guides above on how to access the options from within your Editor (e.g. Unity or Blender).
',7),z={href:"https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html",target:"_blank",rel:"noopener noreferrer"},L={href:"https://google.github.io/draco/",target:"_blank",rel:"noopener noreferrer"},Y=e("p",null,"We generally recommend making production builds for optimized file size and loading speed (see more information below).",-1),K=e("h2",{id:"production-builds",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#production-builds","aria-hidden":"true"},"#"),t(" Production Builds")],-1),R={href:"https://github.com/KhronosGroup/KTX-Software/releases",target:"_blank",rel:"noopener noreferrer"},V={href:"https://github.com/KhronosGroup/KTX-Software/releases",target:"_blank",rel:"noopener noreferrer"},X=e("br",null,null,-1),J=e("em",null,"If you're sure that you have installed toktx and it's part of your PATH but still can't be found, please restart your machine and try build again.",-1),$={class:"custom-container details"},Q=e("summary",null,"Advanced: Custom glTF extensions",-1),Z=e("code",null,"gltf-transform",-1),ee={href:"https://www.npmjs.com/package/@needle-tools/gltf-build-pipeline",target:"_blank",rel:"noopener noreferrer"},te=n('
Production builds will by default compress textures using KTX2 (either ETC1S or UASTC depending on their usage in the project) but you can also select WebP compression and select a quality level.
# How do I choose between ETC1S, UASTC and WebP compression?
Format
ETC1S
UASTC
WebP
GPU Memory Usage
Low
Low
High (uncompressed)
File Size
Low
High
Very low
Quality
Medium
Very high
Depends on quality setting
Typical usage
Works for everything, but best for color textures
High-detail data textures: normal maps, roughness, metallic, etc.
Files where ETC1S quality is not sufficient but UASTC is too large
You have the option to select texture compression and progressive loading options per Texture by using the Needle Texture Importer in Unity or in the Material tab in Blender.
Unity: How can I set per-texture compression settings?
Blender: How can I set per-texture compression settings?
Select the material tab. You will see compression options for all textures that are being used by that material.
Toktx can not be found
Windows: Make sure you have added toktx to your system environment variables. You may need to restart your computer after adding it to refresh the environment variables. The default install location is C:\\Program Files\\KTX-Software\\bin
You can also add the Progressive Texture Settings component anywhere in your scene, to make all textures in your project be progressively loaded. Progressive loading is not applied to lightmaps or skybox textures at this point.
With progressive loading textures will first be loaded using a lower resolution version. A full quality version will be loaded dynamically when the texture becomes visible. This usually reduces initial loading of your scene significantly.
How can I enable progressive texture loading?
# Progressive textures can be enabled per texture or for all textures in your project:
# Enable for all textures in the project that don't have any other specific setting:
By default, a production build will compress meshes using Draco compression. Use the MeshCompression component to select between draco and mesh-opt per exported glTF. Additionally you can setup mesh simplification to reduce the polycount for production builds in the mesh import settings (Unity). When viewing your application in the browser, you can append ?wireframe to your URL to preview the meshes.
You can deploy to glitch by adding the DeployToGlitch component to your scene and following the instructions.
Note that free projects hosted on glitch may not exceed ~100 MB. If you need to upload a larger project consider using a different deployment target.
How do I deploy to Glitch from Unity?
Add the DeployToGlitch component to the GameObject that also has the ExportInfo component.
Click the Create new Glitch Remix button on the component
Glitch will now create a remix of the template. Copy the URL from your browser
Open Unity again and paste the URL in the Project Name field of your Deploy To Glitch component
Wait a few seconds until Unity has received your deployment key from glitch (this key is safely stored in the .env file on glitch. Do not share it with others, everyone with this key will be able to upload to your glitch website)
Once the Deploy Key has been received you can click the Build & Deploy button to upload to glitch.
How do I deploy to Glitch from Blender?
Find the Deploy To Glitch panel in the Scene tab
Click the Remix on glitch button on the component
Your browser will open the glitch project template
Wait for Glitch to generate a new project
Copy paste the project URL in the Blender DeployToGlitch panel as the project name (you can paste the full URL, the panel will extract the necessary information)
On Glitch open the .env file and enter a password in the field Variable Value next to the DEPLOY_KEY
Enter the same password in Blender in the Key field
Click the DeployToGlitch button to build and upload your project to glitch. A browser will open when the upload has finished. Try to refresh the page if it shows black after having opened it.
',5),ne=e("code",null,"Create new Glitch Remix",-1),se=e("code",null,"there was an error starting the editor",-1),ae=e("strong",null,"OK",-1),re={href:"https://glitch.com/",target:"_blank",rel:"noopener noreferrer"},le=n('
Just add the DeployToNetlify component to your scene and follow the instructions. You can create new projects with the click of a button or deployt to existing projects.
',4),de={href:"https://github.com/needle-engine/nextjs-sample",target:"_blank",rel:"noopener noreferrer"},ce=e("h3",{id:"deploy-to-itch.io",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#deploy-to-itch.io","aria-hidden":"true"},"#"),t(" Deploy to itch.io")],-1),pe={class:"custom-container details"},he=e("summary",null,"How do I deploy to itch.io from Unity?",-1),ue={href:"https://itch.io/game/new",target:"_blank",rel:"noopener noreferrer"},me=n('
Set Kind of project to HTML
Add the DeployToItch component to your scene and click the Build button
Wait for the build to finish, it will open a folder with the final zip when it has finished
Upload to final zip to itch.io
Select This file will be played in the browser
Save your itch page and view the itch project page. It should now load your Needle Engine project ๐
',6),ge=e("h4",{id:"optional-settings",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#optional-settings","aria-hidden":"true"},"#"),t(" Optional settings")],-1),ye=e("p",null,[e("img",{src:"https://user-images.githubusercontent.com/5083203/191217263-355d9b72-5431-4170-8eca-bfbbb39ae810.png",alt:"image"})],-1),be={class:"custom-container details"},fe=e("summary",null,"Itch.io: failed to find index.html",-1),_e={id:"failed-to-find-index.html",tabindex:"-1"},we=e("p",null,[e("img",{src:"https://user-images.githubusercontent.com/5083203/191213162-2be63e46-2a65-4d41-a713-98c753ccb600.png",alt:"image"}),e("br"),t(" If you see this error after uploading your project make sure you do not upload a gzipped index.html. You can disable gzip compression in "),e("code",null,"vite.config.js"),t(" in your Needle web project folder. Just remove the line with "),e("code",null,"viteCompression({ deleteOriginFile: true })"),t(". The build your project again and upload to itch.")],-1),ve=n('
Add the DeployToFTP componentยน on a GameObject in your scene (it is good practice to add it to the same GameObject as ExportInfo - but it is not mandatory)
Assign an FTP server asset and fill out server, username, and password if you have not already ยฒ This asset contains the access information to your FTP server - you get them when you create a new FTP account at your hosting provider
Click the Build & Deploy button on the DeployToFTP component to build your project and uploading it to your FTP account
ยน Deploy to FTP component
ยฒ FTP Server asset containing the access information of your FTP user account
Deploy To FTP component after server asset is assigned. You can directly deploy to a subfolder on your server using the path field
How do I deploy to my FTP server manually?
Open File > Build Settings, select Needle Engine, and click on Build
Wait for the build to complete - the resulting dist folder will open automatically after all build and compression steps have run.
Copy the files from the dist folder to your FTP storage.
That's it! ๐
Note: If the result doesn't work when uploaded it might be that your web server does not support serving gzipped files. You have two options to fix the problem: Option 1: You can try enabling gzip compression on your server using a htaccess file! Option 2: You can turn gzip compression off in the build settings at File/Build Window and selecting the Needle Engine platform.
Note: If you're getting errors during compression, please let us know and report a bug! If your project works locally and only fails when doing production builds, you can get unstuck right away by doing a Development Build. For that, simply toggle Development Build on in the Build Settings.
',4),xe={class:"custom-container details"},ke=e("summary",null,"How do I deploy to Github Pages from Unity?",-1),Te=e("p",null,[t("Add the DeployToGithubPages component to your scene and copy-paste the github repository (or github pages url) that you want to deploy to."),e("br"),e("img",{src:I,alt:"Deploy To github pages component"})],-1),je=n('
With Needle Engine you can build to Facebook Instant Games automatically No manual adjustments to your web app or game are required.
How do I deploy to Facebook Instant Games from Unity?
Add the Deploy To Facebook Instant Games component to your scene:
Click the Build For Instant Games button
After the build has finished you will get a ZIP file that you can upload to your facebook app.
On Facebook add the Instant Games module and go to Instant Games/Web hosting
You can upload your zip using the Upload version button (1). After the upload has finished and the zip has been processed click the Stage for testing button to test your app (2, here the blue button) or Push to production (the button with the star icon)
That's it - you can then click the Play button next to each version to test your game on facebook.
',3),Pe={class:"custom-container details"},De=e("summary",null,"How do I create a app on Facebook (with Instant Games capabilities)",-1),Ge={href:"https://developers.facebook.com/apps/creation/",target:"_blank",rel:"noopener noreferrer"},Fe=e("code",null,"Other",-1),Ie=e("code",null,"Next",-1),Ue=e("img",{src:B,alt:"Create facebook instant games app"},null,-1),Ne=e("li",null,[e("p",null,[t("Select type "),e("code",null,"Instant Games"),e("img",{src:S,alt:"Create facebook instant games app"})])],-1),Ce=e("li",null,[e("p",null,[t("After creating the app add the "),e("code",null,"Instant Games"),t(" product "),e("img",{src:A,alt:"Add instant games product"})])],-1),Be={href:"https://developers.facebook.com/docs/games/build/instant-games",target:"_blank",rel:"noopener noreferrer"},Se=e("br",null,null,-1),Ae=e("strong",null,"Note",-1),Ee=e("br",null,null,-1),Oe=n('
It's possible to create regular Unity projects where you can build both to Needle Engine and to regular Unity platforms such as Desktop or even WebGL. Our "component mapping" approach means that no runtime logic is modified inside Unity - if you want you can regularily use Play Mode and build to other target platforms. In some cases this will mean that you have duplicate code (C# code and matching TypeScript logic). The amount of extra work through this depends on your project.
Enter Play Mode in Unity In Project Settings > Needle Engine, you can turn off Override Play Mode and Override Build settings to switch between Needle's build process and Unity's build process:
Needle Engine for Unity supports various commandline arguments to export single assets (Prefabs or Scenes) or to build a whole web project in batch mode (windowsless).
The following list gives a table over the available options:
-scene
path to a scene or a asset to be exported e.g. Assets/path/to/myObject.prefab or Assets/path/to/myScene.unity
-outputPath <path/to/output.glb>
set the output path for the build (only valid when building a scene)
-buildProduction
run a production build
-buildDevelopment
run a development build
-debug
open a console window for debugging
',8);function We(He,qe){const i=s("ExternalLinkIcon"),r=s("RouterLink"),l=s("video-embed");return h(),u("div",null,[O,e("div",W,[H,e("p",null,[t("Please let us know in our "),e("a",q,[t("discord"),o(i)]),t("!")])]),M,e("p",null,[t("The main difference to a production build is that it does not perform "),e("a",z,[t("ktx2"),o(i)]),t(" and "),e("a",L,[t("draco"),o(i)]),t(" compression (for reduction of file size and loading speed) as well as the option to progressively load high-quality textures.")]),Y,K,e("p",null,[t("To make a production build, you need to have "),e("a",R,[t("toktx"),o(i)]),t(" installed, which provides texture compression using the KTX2 supercompression format. Please go to the "),e("a",V,[t("toktx Releases Page"),o(i)]),t(" and download and install the latest version (v4.1.0 at the time of writing). You may need to restart Unity after installing it."),X,J]),e("details",$,[Q,e("p",null,[t("If you plan on adding your own custom glTF extensions, building for production requires handling those in "),Z,t(". See "),e("a",ee,[t("@needle-tools/gltf-build-pipeline"),o(i)]),t(" for reference.")])]),te,e("p",null,[e("a",oe,[t("Glitch"),o(i)]),t(" provides a fast and free way for everyone to host small and large websites. We're providing an easy way to remix and deploy to a new Glitch page (based on our starter), and also to run a minimalistic networking server on the same Glitch page if needed.")]),ie,e("p",null,[t("If you click "),ne,t(" and the browser shows an error like "),se,t(" you can click "),ae,t(". Then go to "),e("a",re,[t("glitch.com"),o(i)]),t(" and make sure you are signed in. After that you then try clicking the button again in Unity or Blender.")]),le,e("p",null,[t("See our "),e("a",de,[t("sample project"),o(i)]),t(" for the project configuration")]),ce,e("details",pe,[he,e("ol",null,[e("li",null,[e("p",null,[t("Create a new project on "),e("a",ue,[t("itch.io"),o(i)])])]),me]),ge,ye]),e("details",be,[fe,e("h4",_e,[o(r,{class:"header-anchor",to:"/#failed-to-find-index.html","aria-hidden":"true"},{default:m(()=>[t("#")]),_:1}),t(" Failed to find index.html")]),we]),ve,e("details",xe,[ke,Te,o(l,{src:"https://www.youtube.com/watch?v=Vyk3cWB6u-c"})]),je,e("details",Pe,[De,e("ol",null,[e("li",null,[e("p",null,[e("a",Ge,[t("Create a new app"),o(i)]),t(" and select "),Fe,t(". Then click "),Ie,Ue])]),Ne,Ce]),e("p",null,[t("Here you can find "),e("a",Be,[t("the official instant games documentation"),o(i)]),t(" on facebook."),Se,Ae,t(" that all you have to do is to create an app with instant games capabilities."),Ee,t(" We will take care of everything else and no manual adjustments to your Needle Engine website are required.")])]),Oe])}const Ye=p(E,[["render",We],["__file","deployment.html.vue"]]);export{Ye as default};
diff --git a/assets/docsearch-1d421ddb.js b/assets/docsearch-1d421ddb.js
new file mode 100644
index 000000000..182023d2f
--- /dev/null
+++ b/assets/docsearch-1d421ddb.js
@@ -0,0 +1,2 @@
+const i=`@media (min-width: 751px){#docsearch-container{min-width:171.36px}}@media (max-width: 750px){.DocSearch-Container{position:fixed}#docsearch-container{min-width:52px}}@media print{#docsearch-container{display:none}}
+`;export{i as default};
diff --git a/assets/everywhere-actions.html-2b981029.js b/assets/everywhere-actions.html-2b981029.js
new file mode 100644
index 000000000..5e59b5185
--- /dev/null
+++ b/assets/everywhere-actions.html-2b981029.js
@@ -0,0 +1,23 @@
+import{_ as c,M as s,p as d,q as l,N as n,R as e,t,V as p,a1 as o}from"./framework-c782e227.js";const u="/docs/imgs/everywhere-actions-component-menu.gif",h={},m=o('
Needle's Everywhere Actions are a set of carefully chosen components that allow you to create interactive experiences in Unity without writing a single line of code. They are designed to serve as building blocks for experiences across the web, mobile and XR, including Augmented Reality on iOS.
From low-level triggers and actions, higher-level complex interactive behaviours can be built.
For iOS support add the USDZExporter component to your scene. It is good practice to add it to the same object as the WebXR component (but not mandatory)
To add an action to any object in your scene select it and then click Add Component > Needle > Everywhere Actions > [Action].
Demonstrates spatial audio, animation, and interactions.
',14),k=e("h3",{id:"simple-character-controllers",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#simple-character-controllers","aria-hidden":"true"},"#"),t(" Simple Character Controllers")],-1),b=e("p",null,"Demonstrates combining animations, look at, and movement.",-1),v=e("h3",{id:"image-tracking",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#image-tracking","aria-hidden":"true"},"#"),t(" Image Tracking")],-1),g=e("p",null,"Demonstrates how to attach 3D content onto a custom image marker. Start the scene below in AR and point your phone's camera at the image marker on a screen, or print it out.",-1),f=e("img",{src:"https://engine.needle.tools/samples-uploads/image-tracking/assets/needle-marker.png",alt:"Image Marker",width:"300"},null,-1),w=e("p",null,[e("a",{href:"https://engine.needle.tools/samples-uploads/image-tracking/assets/needle-marker.png",target:"_blank"},"Download Sample Image Marker")],-1),y=e("strong",null,"On Android:",-1),A=e("h3",{id:"interactive-building-blocks-overview",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#interactive-building-blocks-overview","aria-hidden":"true"},"#"),t(" Interactive Building Blocks Overview")],-1),_=e("h2",{id:"create-your-own-everywhere-actions",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#create-your-own-everywhere-actions","aria-hidden":"true"},"#"),t(" Create your own Everywhere Actions")],-1),x=e("p",null,"Creating new Everywhere Actions involves writing code for your action in TypeScript, which will be used in the browser and for WebXR, and using our TriggerBuilder and ActionBuilder API to create a matching setup for Augmented Reality on iOS via QuickLook. When creating custom actions, keep in mind that QuickLook has a limited set of features available. You can still use any code you want for the browser and WebXR, but the behaviour for QuickLook may need to be an approximation built from the available triggers and actions.",-1),B=e("div",{class:"custom-container tip"},[e("p",{class:"custom-container-title"},"TIP"),e("p",null,'Often constructing specific behaviours requires thinking outside the box and creatively applying the available low-level actions. An example would be a "Tap to Place" action โ there is no raycasting or hit testing available in QuickLook, but you could cover the expected placement area with a number of invisible objects and use a "Tap" trigger to move the object to be placed to the position of the tapped invisible object.')],-1),S={href:"https://developer.apple.com/documentation/arkit/usdz_schemas_for_ar/actions_and_triggers",target:"_blank",rel:"noopener noreferrer"},C=o(`
Here's the implementation for HideOnStart as an example for how to create an Everywhere Action with implementations for both the browser and QuickLook:
Often, getting the right behaviour will involve composing higher-level actions from the available lower-level actions. For example, our "Change Material on Click" action is composed of a number of fadeActions and internally duplicates objects with different sets of materials each. By carefully constructing these actions, complex behaviours can be achieved.
To see the implementation of our built-in Everywhere Actions, please take look at src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts.
`,9);function T(j,I){const a=s("sample"),i=s("RouterLink"),r=s("ExternalLinkIcon");return d(),l("div",null,[m,n(a,{src:"https://engine.needle.tools/samples-uploads/musical-instrument"}),k,b,n(a,{src:"https://engine.needle.tools/samples-uploads/usdz-characters"}),v,g,f,w,e("p",null,[y,t(' please turn on "WebXR Incubations" in the Chrome Flags. You can find those by pasting '),n(i,{to:"/chrome:/flags/#webxr-incubations"},{default:p(()=>[t("chrome://flags/#webxr-incubations")]),_:1}),t(" into the Chrome browser address bar of your Android phone.")]),n(a,{src:"https://engine.needle.tools/samples-uploads/image-tracking"}),A,n(a,{src:"https://engine.needle.tools/samples-uploads/usdz-interactivity"}),_,x,B,e("p",null,[t("Triggers and Actions for QuickLook are based on "),e("a",S,[t("Apple's Preliminary Interactive USD Schemas"),n(r)])]),C])}const R=c(h,[["render",T],["__file","everywhere-actions.html.vue"]]);export{R as default};
diff --git a/assets/everywhere-actions.html-4b37437d.js b/assets/everywhere-actions.html-4b37437d.js
new file mode 100644
index 000000000..425dd6b72
--- /dev/null
+++ b/assets/everywhere-actions.html-4b37437d.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-f2e4a738","path":"/everywhere-actions.html","title":"Everywhere Actions","lang":"en-US","frontmatter":{"title":"Everywhere Actions","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/everywhere actions.png"}],["meta",{"name":"og:description","content":"---\\nNeedle's Everywhere Actions are a set of carefully chosen components that allow you to create interactive experiences in Unity without writing a single line of code.\\nThey are designed to serve as building blocks for experiences across the web, mobile and XR, including Augmented Reality on iOS.\\nFrom low-level triggers and actions, higher-level complex interactive behaviours can be built."}]],"description":"---\\nNeedle's Everywhere Actions are a set of carefully chosen components that allow you to create interactive experiences in Unity without writing a single line of code.\\nThey are designed to serve as building blocks for experiences across the web, mobile and XR, including Augmented Reality on iOS.\\nFrom low-level triggers and actions, higher-level complex interactive behaviours can be built."},"headers":[{"level":2,"title":"What are Everywhere Actions?","slug":"what-are-everywhere-actions","link":"#what-are-everywhere-actions","children":[{"level":3,"title":"Supported Platforms","slug":"supported-platforms","link":"#supported-platforms","children":[]}]},{"level":2,"title":"How do I use Everywhere Actions?","slug":"how-do-i-use-everywhere-actions","link":"#how-do-i-use-everywhere-actions","children":[]},{"level":2,"title":"List of Everywhere Actions","slug":"list-of-everywhere-actions","link":"#list-of-everywhere-actions","children":[]},{"level":2,"title":"Samples","slug":"samples","link":"#samples","children":[{"level":3,"title":"Musical Instrument","slug":"musical-instrument","link":"#musical-instrument","children":[]},{"level":3,"title":"Simple Character Controllers","slug":"simple-character-controllers","link":"#simple-character-controllers","children":[]},{"level":3,"title":"Image Tracking","slug":"image-tracking","link":"#image-tracking","children":[]},{"level":3,"title":"Interactive Building Blocks Overview","slug":"interactive-building-blocks-overview","link":"#interactive-building-blocks-overview","children":[]}]},{"level":2,"title":"Create your own Everywhere Actions","slug":"create-your-own-everywhere-actions","link":"#create-your-own-everywhere-actions","children":[{"level":3,"title":"Code Example","slug":"code-example","link":"#code-example","children":[]},{"level":3,"title":"Low level methods for building your own actions","slug":"low-level-methods-for-building-your-own-actions","link":"#low-level-methods-for-building-your-own-actions","children":[]}]}],"git":{},"filePathRelative":"everywhere-actions.md"}`);export{e as data};
diff --git a/assets/examples.html-dc4c7cbc.js b/assets/examples.html-dc4c7cbc.js
new file mode 100644
index 000000000..89c1154a1
--- /dev/null
+++ b/assets/examples.html-dc4c7cbc.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-63f25852","path":"/examples.html","title":"Example Projects โจ","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/examples.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[{"level":2,"title":"Needle Website","slug":"needle-website","link":"#needle-website","children":[]},{"level":2,"title":"Castle Builder","slug":"castle-builder","link":"#castle-builder","children":[]},{"level":2,"title":"Bike Configurator","slug":"bike-configurator","link":"#bike-configurator","children":[]},{"level":2,"title":"Sandbox Template","slug":"sandbox-template","link":"#sandbox-template","children":[]},{"level":2,"title":"Songs of Cultures","slug":"songs-of-cultures","link":"#songs-of-cultures","children":[]},{"level":2,"title":"Pokรฉmon Card","slug":"pok-mon-card","link":"#pok-mon-card","children":[]},{"level":2,"title":"Encryption in Space","slug":"encryption-in-space","link":"#encryption-in-space","children":[]},{"level":2,"title":"Physics Playground","slug":"physics-playground","link":"#physics-playground","children":[]}],"git":{},"filePathRelative":"examples.md"}');export{e as data};
diff --git a/assets/examples.html-ddf0b992.js b/assets/examples.html-ddf0b992.js
new file mode 100644
index 000000000..015675159
--- /dev/null
+++ b/assets/examples.html-ddf0b992.js
@@ -0,0 +1 @@
+import{_ as o,M as r,p as s,q as d,R as e,t,N as a,a1 as i}from"./framework-c782e227.js";const l={},c=i('
',7),h={href:"https://castle.needle.tools",target:"_blank",rel:"noopener noreferrer"},p=e("p",null,"https://user-images.githubusercontent.com/5083203/186145731-705cfec2-1779-4a0b-97d9-95f3edaaf2d0.mp4",-1),u=e("h2",{id:"bike-configurator",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#bike-configurator","aria-hidden":"true"},"#"),t(" Bike Configurator")],-1),f={href:"https://bike.needle.tools",target:"_blank",rel:"noopener noreferrer"},_=e("p",null,"https://user-images.githubusercontent.com/5083203/186146814-52fb05c7-a073-4efa-a226-47a9c1835413.mp4",-1),b=e("h2",{id:"sandbox-template",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#sandbox-template","aria-hidden":"true"},"#"),t(" Sandbox Template")],-1),m={href:"https://fwd.needle.tools/needle-engine/glitch-starter",target:"_blank",rel:"noopener noreferrer"},g=e("p",null,"https://user-images.githubusercontent.com/5083203/186149117-ca7cf22f-dc7d-4c74-86d4-d78fe53a208c.mp4",-1),x=e("h2",{id:"songs-of-cultures",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#songs-of-cultures","aria-hidden":"true"},"#"),t(" Songs of Cultures")],-1),k={href:"https://fwd.needle.tools/needle-engine/projects/songs-of-cultures",target:"_blank",rel:"noopener noreferrer"},y=e("p",null,"https://user-images.githubusercontent.com/5083203/186147814-159a33f9-f1a6-47d4-804f-5f8f5a63125d.mp4",-1),w=e("h2",{id:"pok-mon-card",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#pok-mon-card","aria-hidden":"true"},"#"),t(" Pokรฉmon Card")],-1),N={href:"https://fwd.needle.tools/needle-engine/projects/pokemon-card",target:"_blank",rel:"noopener noreferrer"},S={href:"https://alexanderameye.github.io/notes/holographic-card-shader/",target:"_blank",rel:"noopener noreferrer"},v=e("p",null,"https://user-images.githubusercontent.com/5083203/186149736-49a697b3-4282-4b71-ab13-a6b176955c13.mp4",-1),E=e("h2",{id:"encryption-in-space",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#encryption-in-space","aria-hidden":"true"},"#"),t(" Encryption in Space")],-1),P={href:"https://fwd.needle.tools/needle-engine/projects/encryption",target:"_blank",rel:"noopener noreferrer"},B=e("p",null,"https://user-images.githubusercontent.com/5083203/186151157-0c0a7d05-ad42-44be-b553-8d4cd48cbb81.mp4",-1),C=e("h2",{id:"physics-playground",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#physics-playground","aria-hidden":"true"},"#"),t(" Physics Playground")],-1),j={href:"https://bruno-simon-20k-needle.glitch.me/",target:"_blank",rel:"noopener noreferrer"},V=e("p",null,"https://user-images.githubusercontent.com/5083203/186149536-987ee796-3fe0-42bc-bd80-4c25aaf174aa.mp4",-1);function A(L,T){const n=r("ExternalLinkIcon");return s(),d("div",null,[c,e("p",null,[e("a",h,[t("Play Now โก"),a(n)]),t(" โ by Needle")]),p,u,e("p",null,[e("a",f,[t("Bike Configurator โก"),a(n)]),t(" โ by Needle")]),_,b,e("p",null,[e("a",m,[t("Sandbox Template โก"),a(n)]),t(" โ by Needle")]),g,x,e("p",null,[e("a",k,[t("Songs of Cultures โก"),a(n)]),t(" โ by A.MUSE")]),y,w,e("p",null,[e("a",N,[t("Pokรฉmon Card โก"),a(n)]),t(" โ Scene from Alex Ameye โข "),e("a",S,[t("Original Blog Post by Alex โก"),a(n)])]),v,E,e("p",null,[e("a",P,[t("Encryption in Space โก"),a(n)]),t(" โ by Katja Rempel & Nick Jwu")]),B,C,e("p",null,[e("a",j,[t("Physics Playground โก"),a(n)]),t(" โ Scene from Bruno Simon")]),V])}const M=o(l,[["render",A],["__file","examples.html.vue"]]);export{M as default};
diff --git a/assets/export.html-07734cf5.js b/assets/export.html-07734cf5.js
new file mode 100644
index 000000000..1f453ebf6
--- /dev/null
+++ b/assets/export.html-07734cf5.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-99bff5e8","path":"/export.html","title":"Exporting Assets to glTF","lang":"en-US","frontmatter":{"title":"Exporting Assets to glTF","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/exporting assets to gltf.png"}],["meta",{"name":"og:description","content":"---\\nAdd an ExportInfo component to your Unity scene to generate a new web project from a template, link to an existing web project that you want to export to, set up dependencies to other libraries and packages and to deploy your project.\\nBy default, your scene is exported on save. This setting can be changed by disabling Auto Export in the ExportInfo component.\\nTo export meshes, materials, animations, textures (...) create a new GameObject in your hierarchy and add a GltfObject component to it. This is the root of a new glTF file. It will be exported whenever you make a change to the scene and save.\\nOnly scripts and data on and inside those root objects is exported. Scripts and data outside of them are not exported.\\nAdd a cube as a child of your root object and save your scene. Note that the output assets/ folder (see project structure) now contains a new .glb file with the same name as your root GameObject.\\nYou can enable the Smart Export setting (via Edit/Project Settings/Needle ) to only export when a change in this object's hierarchy is detected.\\n:::details How to prevent specific objects from being exported\\nObjects with the EditorOnly tag will be ignored on export including their child hierarchy.\\nBe aware that this is preferred over disabling objects as disabled will still get exported in case they're turned on later.\\n:::\\nIf you want to split up your application into multiple levels or scenes then you can simply use the SceneSwitcher component. You can then structure your application into multiple scenes or prefabs and add them to the SceneSwitcher array to be loaded and unloaded at runtime. This is a great way to avoid having to load all your content upfront and to keep loading times small (for example i"}]],"description":"---\\nAdd an ExportInfo component to your Unity scene to generate a new web project from a template, link to an existing web project that you want to export to, set up dependencies to other libraries and packages and to deploy your project.\\nBy default, your scene is exported on save. This setting can be changed by disabling Auto Export in the ExportInfo component.\\nTo export meshes, materials, animations, textures (...) create a new GameObject in your hierarchy and add a GltfObject component to it. This is the root of a new glTF file. It will be exported whenever you make a change to the scene and save.\\nOnly scripts and data on and inside those root objects is exported. Scripts and data outside of them are not exported.\\nAdd a cube as a child of your root object and save your scene. Note that the output assets/ folder (see project structure) now contains a new .glb file with the same name as your root GameObject.\\nYou can enable the Smart Export setting (via Edit/Project Settings/Needle ) to only export when a change in this object's hierarchy is detected.\\n:::details How to prevent specific objects from being exported\\nObjects with the EditorOnly tag will be ignored on export including their child hierarchy.\\nBe aware that this is preferred over disabling objects as disabled will still get exported in case they're turned on later.\\n:::\\nIf you want to split up your application into multiple levels or scenes then you can simply use the SceneSwitcher component. You can then structure your application into multiple scenes or prefabs and add them to the SceneSwitcher array to be loaded and unloaded at runtime. This is a great way to avoid having to load all your content upfront and to keep loading times small (for example i"},"headers":[{"level":2,"title":"๐ฆ Exporting glTF files","slug":"exporting-gltf-files","link":"#exporting-gltf-files","children":[{"level":3,"title":"Lazy loading and multiple levels / scenes","slug":"lazy-loading-and-multiple-levels-scenes","link":"#lazy-loading-and-multiple-levels-scenes","children":[]},{"level":3,"title":"Recommended Complexity per glTF","slug":"recommended-complexity-per-gltf","link":"#recommended-complexity-per-gltf","children":[]},{"level":3,"title":"Prefabs","slug":"prefabs","link":"#prefabs","children":[]},{"level":3,"title":"Scene Assets","slug":"scene-assets","link":"#scene-assets","children":[]}]},{"level":2,"title":"๐ Exporting Animations","slug":"exporting-animations","link":"#exporting-animations","children":[]},{"level":2,"title":"๐ Exporting the Skybox","slug":"exporting-the-skybox","link":"#exporting-the-skybox","children":[]},{"level":2,"title":"โจ Exporting Materials","slug":"exporting-materials","link":"#exporting-materials","children":[{"level":3,"title":"Physically Based Materials (PBR)","slug":"physically-based-materials-pbr","link":"#physically-based-materials-pbr","children":[]},{"level":3,"title":"Custom Shaders","slug":"custom-shaders","link":"#custom-shaders","children":[]}]},{"level":2,"title":"๐ก Exporting Lightmaps","slug":"exporting-lightmaps","link":"#exporting-lightmaps","children":[{"level":3,"title":"Recommended Lightmap Settings","slug":"recommended-lightmap-settings","link":"#recommended-lightmap-settings","children":[]},{"level":3,"title":"Mixing Baked and Non-Baked Objects","slug":"mixing-baked-and-non-baked-objects","link":"#mixing-baked-and-non-baked-objects","children":[]}]}],"git":{},"filePathRelative":"export.md"}`);export{e as data};
diff --git a/assets/export.html-ffd2f8ff.js b/assets/export.html-ffd2f8ff.js
new file mode 100644
index 000000000..54d9cd095
--- /dev/null
+++ b/assets/export.html-ffd2f8ff.js
@@ -0,0 +1,26 @@
+import{_ as r,M as i,p as l,q as c,R as t,t as e,N as a,V as d,a1 as s}from"./framework-c782e227.js";const p={},h=s('
Add an ExportInfo component to your Unity scene to generate a new web project from a template, link to an existing web project that you want to export to, set up dependencies to other libraries and packages and to deploy your project.
By default, your scene is exported on save. This setting can be changed by disabling Auto Export in the ExportInfo component.
To export meshes, materials, animations, textures (...) create a new GameObject in your hierarchy and add a GltfObject component to it. This is the root of a new glTF file. It will be exported whenever you make a change to the scene and save.
Only scripts and data on and inside those root objects is exported. Scripts and data outside of them are not exported.
Add a cube as a child of your root object and save your scene. Note that the output assets/ folder (see project structure) now contains a new .glb file with the same name as your root GameObject.
You can enable the Smart Export setting (via Edit/Project Settings/Needle ) to only export when a change in this object's hierarchy is detected.
How to prevent specific objects from being exported
Objects with the EditorOnly tag will be ignored on export including their child hierarchy. Be aware that this is preferred over disabling objects as disabled will still get exported in case they're turned on later.
',10),u=t("code",null,"SceneSwitcher",-1),m={href:"https://needle.tools",target:"_blank",rel:"noopener noreferrer"},g=t("h3",{id:"recommended-complexity-per-gltf",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#recommended-complexity-per-gltf","aria-hidden":"true"},"#"),e(" Recommended Complexity per glTF")],-1),b=t("ul",null,[t("li",null,"Max. 50 MB export size uncompressed (usually ends up ~10-20 MB compressed)"),t("li",null,"Max. 500k vertices (less if you target mobile VR as well)"),t("li",null,"Max. 4x 2k lightmaps")],-1),f=t("p",null,"The scene complexity here is recommended to ensure good performance across a range of web-capable devices and bandwidths. There's no technical limitation to this beyond the capabilities of your device.",-1),y=t("h3",{id:"prefabs",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#prefabs","aria-hidden":"true"},"#"),e(" Prefabs")],-1),x={href:"https://fwd.needle.tools/needle-engine/docs/addressables",target:"_blank",rel:"noopener noreferrer"},v=s('
Exporting Prefabs works with nesting too: a component in a Prefab can reference another Prefab which will then also be exported. This mechanism allows for composing scenes to be as lightweight as possible and loading the most important content first and defer loading of additional content.
Similar to Prefab assets, you can reference other Scene assets. To get started, create a component in Unity with a UnityEditor.SceneAsset field and add it to one of your GameObjects inside a GltfObject. The referenced scene will now be exported as a separate glTF file and can be loaded/deserialized as a AssetReference from TypeScript.
You can keep working inside a referenced scene and still update your main exporter scene/website. On scene save or play mode change we will detect if the current scene is being used by your currently running server and then trigger a re-export for only that glb. (This check is done by name - if a glb inside your <web_project>/assets/ folder exists, it is exported again and the main scene reloads it.)
If you want to reference and load a prefab from one of your scripts you can declare a AssetReference type. Here is a minimal example:
import{ Behaviour, serializable, AssetReference }from"@needle-tools/engine";
+
+exportclassMyClassextendsBehaviour{
+
+ // if you export a prefab or scene as a reference from Unity you'll get a path to that asset
+ // which you can de-serialize to AssetReference for convenient loading
+ @serializable(AssetReference)
+ myPrefab?: AssetReference;
+
+ asyncstart(){
+ // directly instantiate
+ const myInstance =awaitthis.myPrefab?.instantiate();
+
+ // you can also just load and instantiate later
+ // const myInstance = await this.myPrefab.loadAssetAsync();
+ // this.gameObject.add(myInstance)
+ // this is useful if you know that you want to load this asset only once because it will not create a copy
+ // since \`\`instantiate()\`\` does create a copy of the asset after loading it
+ }
+}
+
One current limitation is that materials won't be duplicated on export โ if you want to animate the same material with different colors, for example, you currently need to split the material in two.
By default, materials are converted into glTF materials on export. glTF supports a physically based material model and has a number of extensions that help to represent complex materials.
For full control over what gets exported, it's highly recommended to use the glTF materials provided by UnityGltf:
PBRGraph
UnlitGraph
These materials are exported as-is, with no conversion necessary. They allow for using advanced material properties such as refractive transmission and iridescence, which can be exported as well.
Materials that can be converted out-of-the-box:
BiRP/Standard
BiRP/Autodesk Interactive
BiRP/Unlit
URP/Lit
URP/Unlit
Other materials are converted using a propery name heuristic. That means that depending on what property names your materials and shaders use, you might want to either refactor your custom shader's properties to use the property names of either URP/Lit or PBRGraph, or export the material as Custom Shader.
To export custom shaders (e.g. ShaderGraph shaders), add an ExportShader Asset Label (see bottom of the inspector) to the shader you want to export.
WARNING
Please see limitations listed below
Note that Custom Shaders aren't part of the ratified glTF material model. The resulting GLB files will not display correctly in other viewers (the materials will most likely display white).
We currently only support custom Unlit shaders โ Lit shader conversion is not officially supported.
Custom Lit Shaders are currently experimental. Not all rendering modes are supported.
Shadow receiving on custom shaders is not supported
Skinned meshes with custom shaders are not supported
As there's multiple coordinate system changes when going from Unity to three.js and glTF, there might be some changes necessary to get advanced effects to work. We try to convert data on export but may not catch all cases where conversions are necessary. These coordinate changes are
UV coordinates in Unity start at the bottom left; in glTF they start at the top left.
X axis values are flipped in glTF compared to Unity (a variant of a left-handed to right-handed coordinate system change).
When working on multiple scenes, disable "Auto Generate" and bake lightmaps explicitly. Otherwise, Unity will discard temporary lightmaps on scene change.
There's no 100% mapping between how Unity handles lights and environment and how three.js handle that. For example, Unity has entirely separate code paths for lightmapped and non-lightmapped objects (lightmapped objects don't receive ambient light since that is already baked into their maps), and three.js doesn't distinguish in that way.
This means that to get best results, we currently recommend specific settings if you're mixing baked and non-baked objects in a scene:
Environment Lighting: Skybox
+Ambient Intensity: 1
+Ambient Color: black
+
2021.3+
2020.3+
If you have no baked objects in your scene, then the following settings should also yield correct results:
Environment Lighting: Color
+Ambient Color: any
+
`,12);function L(E,P){const n=i("ExternalLinkIcon"),o=i("RouterLink");return l(),c("div",null,[h,t("p",null,[e("If you want to split up your application into multiple levels or scenes then you can simply use the "),u,e(" component. You can then structure your application into multiple scenes or prefabs and add them to the SceneSwitcher array to be loaded and unloaded at runtime. This is a great way to avoid having to load all your content upfront and to keep loading times small (for example it is what we did on "),t("a",m,[e("needle.tools"),a(n)]),e(" by separating each section of your website into its own scene and only loading them when necessary)")]),g,b,t("p",null,[e("You can split up scenes and prefabs into multiple glTF files, and then load those on demand (only when needed). This keeps loading performance fast and file size small. See the "),a(o,{to:"/scripting.html#assetreference-and-addressables"},{default:d(()=>[e("AssetReference section in the Scripting docs")]),_:1}),e(".")]),f,y,t("p",null,[e("Prefabs can be exported as invidual glTF files and instantiated at runtime. To export a prefab as glTF just reference a prefab asset (from the project browser and not in the scene) "),t("a",x,[e("from one of your scripts"),a(n)]),e(".")]),v,t("p",null,[e("As an example on "),t("a",k,[e("our website"),a(n)]),e(" each section is setup as a separate scene and on export packed into multiple glb files that we load on demand:")]),w,t("p",null,[e("Needle Engine is one of the first to support the new "),t("a",_,[e("glTF extension KHR_ANIMATION_POINTER"),a(n)]),e("."),T,e(" This means that almost all properties, including script variables, are animatable.")]),S,t("p",null,[e("To export lightmaps simply "),t("a",j,[e("generate lightmaps"),a(n)]),e(" in Unity. Lightmaps will be automatically exported.")]),A])}const U=r(p,[["render",L],["__file","export.html.vue"]]);export{U as default};
diff --git a/assets/faq.html-49435221.js b/assets/faq.html-49435221.js
new file mode 100644
index 000000000..c131db014
--- /dev/null
+++ b/assets/faq.html-49435221.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-092a1d7c","path":"/faq.html","title":"Questions and Answers (FAQ) ๐ก","lang":"en-US","frontmatter":{"title":"Questions and Answers (FAQ) ๐ก","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/questions and answers.png"}],["meta",{"name":"og:description","content":"---\\nThis usually happens when you're using custom shaders or materials and their properties don't cleanly translate to known property names for glTF export.\\nYou can either make sure you're using glTF-compatible materials and shaders, or mark shaders as 'custom' to export them directly."}]],"description":"---\\nThis usually happens when you're using custom shaders or materials and their properties don't cleanly translate to known property names for glTF export.\\nYou can either make sure you're using glTF-compatible materials and shaders, or mark shaders as 'custom' to export them directly."},"headers":[{"level":2,"title":"My objects are white after export","slug":"my-objects-are-white-after-export","link":"#my-objects-are-white-after-export","children":[]},{"level":2,"title":"There's a SSL error when opening the local website","slug":"there-s-a-ssl-error-when-opening-the-local-website","link":"#there-s-a-ssl-error-when-opening-the-local-website","children":[]},{"level":2,"title":"My local website stays black","slug":"my-local-website-stays-black","link":"#my-local-website-stays-black","children":[]},{"level":2,"title":"How to fix Uncaught ReferenceError: NEEDLE_ENGINE_META is not defined / NEEDLE_USE_RAPIER is not defined","slug":"how-to-fix-uncaught-referenceerror-needle-engine-meta-is-not-defined-needle-use-rapier-is-not-defined","link":"#how-to-fix-uncaught-referenceerror-needle-engine-meta-is-not-defined-needle-use-rapier-is-not-defined","children":[]},{"level":2,"title":"THREE.EXRLoader: provided file doesnt appear to be in OpenEXR format","slug":"three.exrloader-provided-file-doesnt-appear-to-be-in-openexr-format","link":"#three.exrloader-provided-file-doesnt-appear-to-be-in-openexr-format","children":[]},{"level":2,"title":"My website becomes too large / is loading slow (too many MB)","slug":"my-website-becomes-too-large-is-loading-slow-too-many-mb","link":"#my-website-becomes-too-large-is-loading-slow-too-many-mb","children":[]},{"level":2,"title":"My scripts don't work after export","slug":"my-scripts-don-t-work-after-export","link":"#my-scripts-don-t-work-after-export","children":[]},{"level":2,"title":"My lightmaps look different / too bright","slug":"my-lightmaps-look-different-too-bright","link":"#my-lightmaps-look-different-too-bright","children":[]},{"level":2,"title":"My scene is too bright / lighting looks different than in Unity","slug":"my-scene-is-too-bright-lighting-looks-different-than-in-unity","link":"#my-scene-is-too-bright-lighting-looks-different-than-in-unity","children":[]},{"level":2,"title":"My skybox resolution is low? How to change my skybox resolution","slug":"my-skybox-resolution-is-low-how-to-change-my-skybox-resolution","link":"#my-skybox-resolution-is-low-how-to-change-my-skybox-resolution","children":[]},{"level":2,"title":"My Shadows are not visible or cut off","slug":"my-shadows-are-not-visible-or-cut-off","link":"#my-shadows-are-not-visible-or-cut-off","children":[]},{"level":2,"title":"My colors look wrong","slug":"my-colors-look-wrong","link":"#my-colors-look-wrong","children":[]},{"level":2,"title":"I'm using networking and Glitch and it doesn't work if more than 30 people visit the Glitch page at the same time","slug":"i-m-using-networking-and-glitch-and-it-doesn-t-work-if-more-than-30-people-visit-the-glitch-page-at-the-same-time","link":"#i-m-using-networking-and-glitch-and-it-doesn-t-work-if-more-than-30-people-visit-the-glitch-page-at-the-same-time","children":[]},{"level":2,"title":"My website doesn't have AR/VR buttons","slug":"my-website-doesn-t-have-ar-vr-buttons","link":"#my-website-doesn-t-have-ar-vr-buttons","children":[]},{"level":2,"title":"I created a new script in a sub-scene but it does not work","slug":"i-created-a-new-script-in-a-sub-scene-but-it-does-not-work","link":"#i-created-a-new-script-in-a-sub-scene-but-it-does-not-work","children":[]},{"level":2,"title":"My local server does not start / I do not see a website","slug":"my-local-server-does-not-start-i-do-not-see-a-website","link":"#my-local-server-does-not-start-i-do-not-see-a-website","children":[]},{"level":2,"title":"Does C# component generation work with javascript only too?","slug":"does-c-component-generation-work-with-javascript-only-too","link":"#does-c-component-generation-work-with-javascript-only-too","children":[]},{"level":2,"title":"I don't have any buttons like \\"Generate Project\\" in my components/inspector","slug":"i-don-t-have-any-buttons-like-generate-project-in-my-components-inspector","link":"#i-don-t-have-any-buttons-like-generate-project-in-my-components-inspector","children":[]},{"level":2,"title":"Toktx can not be found / toktx is not installed","slug":"toktx-can-not-be-found-toktx-is-not-installed","link":"#toktx-can-not-be-found-toktx-is-not-installed","children":[]},{"level":2,"title":"Installing the web project takes forever / does never finish / EONET: no such file or directory","slug":"installing-the-web-project-takes-forever-does-never-finish-eonet-no-such-file-or-directory","link":"#installing-the-web-project-takes-forever-does-never-finish-eonet-no-such-file-or-directory","children":[]},{"level":2,"title":"NPM install fails and there are errors about hard drive / IO","slug":"npm-install-fails-and-there-are-errors-about-hard-drive-io","link":"#npm-install-fails-and-there-are-errors-about-hard-drive-io","children":[]},{"level":2,"title":"I'm getting errors with \\"Unexpected token @. Expected identifier, string literal, numeric literal or ...\\"","slug":"i-m-getting-errors-with-unexpected-token-.-expected-identifier-string-literal-numeric-literal-or-...","link":"#i-m-getting-errors-with-unexpected-token-.-expected-identifier-string-literal-numeric-literal-or-...","children":[]},{"level":2,"title":"I'm getting an error 'failed to load config ... vite.config.js' when running npm commands on Mac OS","slug":"i-m-getting-an-error-failed-to-load-config-...-vite.config.js-when-running-npm-commands-on-mac-os","link":"#i-m-getting-an-error-failed-to-load-config-...-vite.config.js-when-running-npm-commands-on-mac-os","children":[]},{"level":2,"title":"Circular reference error","slug":"circular-reference-error","link":"#circular-reference-error","children":[]},{"level":2,"title":"My scene is not loading and the console contains a warning with 'circular references' or 'failed to update active state'","slug":"my-scene-is-not-loading-and-the-console-contains-a-warning-with-circular-references-or-failed-to-update-active-state","link":"#my-scene-is-not-loading-and-the-console-contains-a-warning-with-circular-references-or-failed-to-update-active-state","children":[]},{"level":2,"title":"Does my machine support WebGL 2?","slug":"does-my-machine-support-webgl-2","link":"#does-my-machine-support-webgl-2","children":[]},{"level":2,"title":"Still have questions? ๐ฑ","slug":"still-have-questions","link":"#still-have-questions","children":[]}],"git":{},"filePathRelative":"faq.md"}`);export{e as data};
diff --git a/assets/faq.html-e9028c0f.js b/assets/faq.html-e9028c0f.js
new file mode 100644
index 000000000..26f89f2d5
--- /dev/null
+++ b/assets/faq.html-e9028c0f.js
@@ -0,0 +1,16 @@
+import{_ as l}from"./ktx-env-variable-d006aea1.js";import{_ as c,M as r,p as d,q as p,R as e,t,N as n,V as i,a1 as o}from"./framework-c782e227.js";const h="/docs/faq/lightmap_encoding.jpg",u={},m=o('
This usually happens when you're using custom shaders or materials and their properties don't cleanly translate to known property names for glTF export. You can either make sure you're using glTF-compatible materials and shaders, or mark shaders as "custom" to export them directly.
Read more about recommended glTF workflows:
Read more about custom shaders:
# There's a SSL error when opening the local website
This is expected. We're enforcing HTTPS to make sure that WebXR and other modern web APIs work out-of-the-box, but that means some browsers complain that the SSL connection (between your local development server and the local website) can't be verified. It also prevents Automatic Reload from working on iOS and MacOS.
If that happens there's usually an exception either in engine code or your code. Open the dev tools (Ctrl + Shift + I or F12 in Chrome) and check the Console for errors. In some cases, especially when you just updated the Needle Engine package version, this can be fixed by stopping and restarting the local dev server. For that, click on the running progress bar in the bottom right corner of the Editor, and click the little X to cancel the running task. Then, simply press Play again.
# How to fix Uncaught ReferenceError: NEEDLE_ENGINE_META is not defined / NEEDLE_USE_RAPIER is not defined
If you are using vite or next.js make sure to add the Needle Engine plugins to your config. Example for vite:
# THREE.EXRLoader: provided file doesnt appear to be in OpenEXR format
Please make sure that sure that you have set Lightmap Encoding to Normal Quality. Go to Edit/Project Settings/Player for changing the setting.
# My website becomes too large / is loading slow (too many MB)
This can have many reasons, but a few common ones are:
too many textures or textures are too large
meshes have too many vertices
meshes have vertex attributes you don't actually need (e.g. have normals and tangents but you're not using them)
objects are disabled and not ignored โ disabled objects get exported as well in case you want to turn them on at runtime! Set their Tag to EditorOnly to completely ignore them for export.
you have multiple GltfObject components in your scene and they all have EmbedSkybox enabled (you need to have the skybox only once per scene you export)
',15),f=e("strong",null,"try to split up your content into multiple glb files",-1),b=e("h2",{id:"my-scripts-don-t-work-after-export",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#my-scripts-don-t-work-after-export","aria-hidden":"true"},"#"),t(" My scripts don't work after export")],-1),k=e("ul",null,[e("li",null,[t("Your existing C# code will "),e("em",null,"not"),t(" export as-is, you have to write matching typescript / javascript for it.")]),e("li",null,"Needle uses typescript / javascript for components and generates C# stubs for them."),e("li",null,"Components that already have matching JS will show that in the Inspector.")],-1),y=e("h2",{id:"my-lightmaps-look-different-too-bright",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#my-lightmaps-look-different-too-bright","aria-hidden":"true"},"#"),t(" My lightmaps look different / too bright")],-1),w={href:"https://docs.needle.tools/lightmaps",target:"_blank",rel:"noopener noreferrer"},v={href:"https://github.com/needle-tools/needle-engine-support/blob/main/documentation/export.md#mixing-baked-and-non-baked-objects",target:"_blank",rel:"noopener noreferrer"},x=o('
# My scene is too bright / lighting looks different than in Unity
Make sure that your lights are set to "Baked" or "Realtime". "Mixed" is currently not supported.
Lights set to mixed (with lightmapping) do affect objects twice in three.js, since there is currently no way to exclude lightmapped objects from lighting
The Intensity Multiplier factor for Skybox in Lighting/Environment is currently not supported and has no effect in Needle Engine
Light shadow intensity can currently not be changed due to a three.js limitation.
Your light has shadows enabled (either Soft Shadow or Hard Shadow)
Your objects are set to "Cast Shadows: On" (see MeshRenderer component)
For directional lights the position of the light is currently important since the shadow camera will be placed where the light is located in the scene.
# I'm using networking and Glitch and it doesn't work if more than 30 people visit the Glitch page at the same time
',9),S=e("li",null,"Deploying on Glitch is a fast way to prototype and might even work for some small productions. The little server there doesn't have the power and bandwidth to host many people in a persistent session.",-1),E={href:"https://www.npmjs.com/package/@needle-tools/needle-tiny-networking-ws",target:"_blank",rel:"noopener noreferrer"},T=o('
Make sure to add the WebXR component somewhere inside your root GltfObject.
Optionally add a AR Session Root component on your root GltfObject or within the child hierarchy to specify placement, scale and orientation for WebXR.
Optionally add a XR Rig component to control where users start in VR
# I created a new script in a sub-scene but it does not work
When creating new scripts in npmdefs in sub-scenes (that is a scene that is exported as a reference from a script in your root export scene) you currently have to re-export the root scene again. This is because the code-gen that is responsible for registering new scripts currently only runs for scenes with a ExportInfo component. This will be fixed in the future.
# My local server does not start / I do not see a website
The most likely reason is an incorrect installation. Check the console and the ExportInfo component for errors or warnings.
If these warnings/errors didn't help, try the following steps in order. Give them some time to complete. Stop once your problem has been resolved. Check the console for warnings and errors.
',7),I=o("
Install your project by selecting your ExportInfo component and clicking Install
Run a clean installation by selecting your ExportInfo component, holding Alt and clicking Clean Install
Try opening your web project directory in a command line tool and follow these steps:
run npm install and then npm run dev-host
Make sure both the local runtime package (node_modules/@needle-tools/engine) as well as three.js (node_modules/three) did install.
You may run npm install in both of these directories as well.
",3),q=o(`
# Does C# component generation work with javascript only too?
While generating C# components does technically run with vanilla javascript too we don't recommend it and fully support it since it is more guesswork or simply impossible for the generator to know which C# type to create for your javascript class. Below you find a minimal example on how to generate a Unity Component from javascript if you really want to tho.
`,6),M={href:"http://localhost:8080/docs/getting-started.html#install-these-tools-for-production-builds",target:"_blank",rel:"noopener noreferrer"},R=e("li",null,[e("p",null,[t("On Windows: Make sure you have added toktx to your system environment variables. You may need to restart your computer after adding it to refresh the environment variables. The default install location is "),e("code",null,"C:\\Program Files\\KTX-Software\\bin")])],-1),C=o('
# Installing the web project takes forever / does never finish / EONET: no such file or directory
Make sure to not create a project on a drive formatted as exFAT because exFAT does not support symlinks, which is required for Needle Engine for Unity prior to version 3.x. You can check the formatting of your drives using the following steps:
Open "System Information" (either windows key and type that or enter "msinfo32" in cmd)
Select Components > Storage > Drives
Select all (Ctrl + A) on the right side of the screen and copy that (Ctrl + C) and paste here (Ctrl + V)
# NPM install fails and there are errors about hard drive / IO
Make sure your project is on a disk that is known to work with node.js. Main reason for failures is that the disk doesn't support symlinks (symbolic links / softlinks), which is a requirement for proper functioning of node.js. NTFS formatting should always work. Known problematic file system formattings are exFAT and FAT32.
To check the format of your drives, you can:
Open "System Information" (either Windows key and type "System Information" or enter msinfo32 in cmd Windows + R)
Select "Components > Storage > Drives"
There, you can see all drives and their formatting listed. Put your projects on a drive that is NTFS formatted.
# I'm getting errors with "Unexpected token @. Expected identifier, string literal, numeric literal or ..."
Needle Engine uses typescript decorators for serialization. To fix this error make sure to enable experimentalDecorators in your tsconfig.json
# I'm getting an error 'failed to load config ... vite.config.js' when running npm commands on Mac OS
You're likely using an x86_64 version of Unity on an (ARM) Apple Silicon processor. Unity 2020.3 is only available for x86_64, later versions also have Apple Silicon versions. Our Unity integration calling npm will thus do so from an x86_64 process, resulting in the x86_64 version of node and vite/esbuild being used. When you afterwards try to run npm commands in the same project from an Apple Silicon app (e.g. VS Code), npm will complain about mismatching architectures with a long error message.
To fix this, use an Apple Silicon version of Unity (2021.1 or later).
You can also temporarily fix it on 2020.3 by deleting the node_modules folder and running npm install again from VS Code. You'll have to delete node_modules again when you switch back to Unity.
This can happen when you have e.g. a SceneSwitcher (or any other component that loads a scene or asset) and the referenced Asset in Unity contains a GltfObject that has the same name as your original scene with the SceneSwitcher. You can double check this in Unity if you get an error that says something like:
Failed to export โ YourSceneName.glb
+you seem to have objects with the same name referencing each other.
+
To fix this you can:
Remove the GltfObject in the referenced Prefab or Scene
Rename the GameObject with the component that loads the referenced scenes
',3),O={href:"https://get.webgl.org/webgl2/",target:"_blank",rel:"noopener noreferrer"},P=e("h4",{id:"known-devices-to-cause-issues",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#known-devices-to-cause-issues","aria-hidden":"true"},"#"),t(" Known devices to cause issues:")],-1),L=e("ul",null,[e("li",null,"Lenovo Thinkpad - T495")],-1),G=e("h2",{id:"still-have-questions",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#still-have-questions","aria-hidden":"true"},"#"),t(" Still have questions? ๐ฑ")],-1),F={href:"https://discord.needle.tools",target:"_blank",rel:"noopener noreferrer"},U=e("p",null,[e("a",{href:"https://discord.needle.tools",target:"_blank"},[e("img",{height:"20",src:"https://img.shields.io/discord/717429793926283276?color=5562ea&label=Discord"})])],-1);function Y(W,D){const s=r("RouterLink"),a=r("ExternalLinkIcon");return d(),p("div",null,[m,e("p",null,[t("See "),n(s,{to:"/testing.html"},{default:i(()=>[t("the Testing docs")]),_:1}),t(" for information on how to set up a self-signed certificate for a smoother development experience.")]),g,e("p",null,[t("If loading time itself is an issue you can "),f,t(" and load them on-demand (this is what we do on our website). For it to work you can put your content into Prefabs or Scenes and reference them from any of your scripts. Please have a look at "),n(s,{to:"/scripting.html#assetreference-and-addressables"},{default:i(()=>[t("Scripting/Addressables in the documentation")]),_:1}),t(".")]),b,k,y,e("p",null,[t("Ensure you're following "),e("a",w,[t("best practices for lightmaps"),n(a)]),t(" and read about "),e("a",v,[t("mixing baked and non-baked objects"),n(a)])]),x,e("p",null,[t("Also see the docs on "),e("a",_,[t("mixing baked and non-baked objects"),n(a)]),t(".")]),j,e("ul",null,[S,e("li",null,[t("We're working on other networking ideas, but in the meantime you can host the website somewhere else (with node.js support) or simply remix it to distribute load among multiple servers. You can also host the "),e("a",E,[t("networking backend package"),n(a)]),t(" itself somewhere else where it can scale e.g. Google Cloud.")])]),T,e("ul",null,[e("li",null,[t("Make sure you follow the "),n(s,{to:"/getting-started/#prerequisites"},{default:i(()=>[t("Prerequisites")]),_:1}),t(".")]),I]),q,e("ul",null,[e("li",null,[e("p",null,[t("Make sure to "),e("a",M,[t("download and install toktx"),n(a)])])]),R]),C,e("p",null,[t("If this doesn't fix the problem please ask "),e("a",N,[t("in our discord"),n(a)]),t(".")]),A,e("p",null,[t("Use a detector "),e("a",O,[t("like this one"),n(a)]),t(" to determine if your device supports WebGL 2, it also hints at what could be the cause of your problem, but generally make sure you have updated your browser and drivers. WebGL 1 is not supported.")]),P,L,G,e("p",null,[e("a",F,[t("Ask in our friendly discord community"),n(a)])]),U])}const B=c(u,[["render",Y],["__file","faq.html.vue"]]);export{B as default};
diff --git a/assets/features-overview.html-94066f50.js b/assets/features-overview.html-94066f50.js
new file mode 100644
index 000000000..f0e75548a
--- /dev/null
+++ b/assets/features-overview.html-94066f50.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-bcfe00ae","path":"/features-overview.html","title":"Feature Overview","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/features overview.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[{"level":2,"title":"Shaders and Materials","slug":"shaders-and-materials","link":"#shaders-and-materials","children":[]},{"level":2,"title":"Crossplatform: VR, AR, Mobile, Desktop","slug":"crossplatform-vr-ar-mobile-desktop","link":"#crossplatform-vr-ar-mobile-desktop","children":[]},{"level":2,"title":"Lightmaps","slug":"lightmaps","link":"#lightmaps","children":[]},{"level":2,"title":"Multiplayer and Networking","slug":"multiplayer-and-networking","link":"#multiplayer-and-networking","children":[]},{"level":2,"title":"Animations and Sequencing","slug":"animations-and-sequencing","link":"#animations-and-sequencing","children":[{"level":3,"title":"Animator","slug":"animator","link":"#animator","children":[]},{"level":3,"title":"Timeline","slug":"timeline","link":"#timeline","children":[]}]},{"level":2,"title":"Physics","slug":"physics","link":"#physics","children":[]},{"level":2,"title":"UI","slug":"ui","link":"#ui","children":[]},{"level":2,"title":"Particles","slug":"particles","link":"#particles","children":[]},{"level":2,"title":"PostProcessing","slug":"postprocessing","link":"#postprocessing","children":[]},{"level":2,"title":"Editor Integrations","slug":"editor-integrations","link":"#editor-integrations","children":[]},{"level":2,"title":"Scripting","slug":"scripting","link":"#scripting","children":[]},{"level":2,"title":"And there is more","slug":"and-there-is-more","link":"#and-there-is-more","children":[]}],"git":{},"filePathRelative":"features-overview.md"}');export{e as data};
diff --git a/assets/features-overview.html-acec4dcb.js b/assets/features-overview.html-acec4dcb.js
new file mode 100644
index 000000000..7ee31c575
--- /dev/null
+++ b/assets/features-overview.html-acec4dcb.js
@@ -0,0 +1 @@
+import{_ as d,M as l,p as c,q as h,R as e,N as n,V as o,t,a1 as u}from"./framework-c782e227.js";const p={},m=e("h1",{id:"feature-overview",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#feature-overview","aria-hidden":"true"},"#"),t(" Feature Overview")],-1),_={class:"table-of-contents"},g=e("h2",{id:"shaders-and-materials",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#shaders-and-materials","aria-hidden":"true"},"#"),t(" Shaders and Materials")],-1),f=e("p",null,"Both PBR Materials and Custom shaders created with Shader Graph or other systems can be exported.",-1),b=e("img",{src:"https://user-images.githubusercontent.com/5083203/186012027-9bbe3944-fa56-41fa-bfbb-c989fa87aebb.png"},null,-1),y={href:"https://unity.com/features/shader-graph",target:"_blank",rel:"noopener noreferrer"},w=e("h2",{id:"crossplatform-vr-ar-mobile-desktop",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#crossplatform-vr-ar-mobile-desktop","aria-hidden":"true"},"#"),t(" Crossplatform: VR, AR, Mobile, Desktop")],-1),k=e("strong",null,"Interactive AR on both Android and iOS",-1),v=e("h2",{id:"lightmaps",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#lightmaps","aria-hidden":"true"},"#"),t(" Lightmaps")],-1),x=e("p",null,[e("img",{src:"https://user-images.githubusercontent.com/5083203/186163693-093c7ae2-96eb-4d75-b98f-bf19f78032ff.gif",alt:"lightmaps"})],-1),S=e("p",null,"Lightmaps can be baked in Unity or Blender to easily add beautiful static light to your 3d content. Lightbaking for the web was never as easy. Just mark objects that you want to lightmap as static in Unity, add one or many lights to your scene (or use emissive materials) and click bake. Needle Engine will export your lightmaps per scene and automatically load and display them just as you see it in the Editor!",-1),A=e("strong",null,"Note",-1),E={href:"https://assetstore.unity.com/packages/tools/level-design/bakery-gpu-lightmapper-122218",target:"_blank",rel:"noopener noreferrer"},R={href:"https://fwd.needle.tools/needle-engine/docs/lightmaps",target:"_blank",rel:"noopener noreferrer"},N=e("h2",{id:"multiplayer-and-networking",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#multiplayer-and-networking","aria-hidden":"true"},"#"),t(" Multiplayer and Networking")],-1),U=e("p",null,"Networking is built into the core runtime. Needle Engine deployments to Glitch come with a tiny server that allows you to deploy a multiplayer 3D environment in seconds. The built-in networked components make it easy to get started, and you can create your own synchronized components. Synchronizing variables and state is super easy!",-1),C={href:"https://fwd.needle.tools/needle-engine/docs/networking",target:"_blank",rel:"noopener noreferrer"},B={href:"https://fwd.needle.tools/needle-engine/docs/scripting",target:"_blank",rel:"noopener noreferrer"},T=e("h2",{id:"animations-and-sequencing",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#animations-and-sequencing","aria-hidden":"true"},"#"),t(" Animations and Sequencing")],-1),P=e("p",null,[t("Needle Engine brings powerful animations, state control and sequencing to the web โ from just playing a single animation to orchestrating and blending complex animations and character controllers. The Exporter can translate Unity components like Animator and Timeline into a web-ready format."),e("br"),t(" We even added this functionality to our Blender addon so you can craft compatible animation state machines and export nla tracks as timelines to the web from within Blender too.")],-1),I=e("h3",{id:"animator",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#animator","aria-hidden":"true"},"#"),t(" Animator")],-1),M=e("img",{src:"https://user-images.githubusercontent.com/5083203/186011302-176524b3-e8e5-4e6e-9b77-7faf3561bb15.png"},null,-1),q={href:"https://docs.unity3d.com/Manual/class-AnimatorController.html",target:"_blank",rel:"noopener noreferrer"},L=e("code",null,"OnStateEnter",-1),O=e("code",null,"OnStateUpdate",-1),V=e("code",null,"OnStateExit",-1),F=e("blockquote",null,[e("p",null,[e("strong",null,"Note"),t(": Sub-states and Blend Trees are not supported.")])],-1),j=e("h3",{id:"timeline",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#timeline","aria-hidden":"true"},"#"),t(" Timeline")],-1),D=e("p",null,[e("img",{src:"https://user-images.githubusercontent.com/5083203/186037829-ee99340d-b19c-484d-b551-94797519c9d9.png",alt:"2022-08-23-013517_Scene"})],-1),G={href:"https://unity.com/features/timeline",target:"_blank",rel:"noopener noreferrer"},W=e("br",null,null,-1),z=e("blockquote",null,[e("p",null,[e("strong",null,"Note"),t(": Sub-Timelines are currently not supported.")])],-1),H=e("strong",null,"Note",-1),J={href:"https://github.com/needle-tools/needle-engine-modules/tree/main/package/TimelineHtml",target:"_blank",rel:"noopener noreferrer"},X=e("h2",{id:"physics",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#physics","aria-hidden":"true"},"#"),t(" Physics")],-1),Y=e("p",null,"Use Rigidbodies, Mesh Colliders, Box Colliders and SphereColliders to add some juicy physics to your world.",-1),K=e("h2",{id:"ui",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#ui","aria-hidden":"true"},"#"),t(" UI")],-1),Q=e("p",null,"Building UI using Unity's UI canvas system is in development. Features currently include exporting Text (including fonts), Images, Buttons.",-1),Z=e("h2",{id:"particles",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#particles","aria-hidden":"true"},"#"),t(" Particles")],-1),$=e("br",null,null,-1),ee={href:"https://engine.needle.tools/samples/particles",target:"_blank",rel:"noopener noreferrer"},te=e("h2",{id:"postprocessing",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#postprocessing","aria-hidden":"true"},"#"),t(" PostProcessing")],-1),ne=e("h2",{id:"editor-integrations",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#editor-integrations","aria-hidden":"true"},"#"),t(" Editor Integrations")],-1),oe=e("p",null,[t("Needle Engine comes with powerful integrations into the Unity Editor and Blender."),e("br"),t(" It allows you to setup and export complex scenes in a visual way providing easy and flexible collaboration between artists and developers.")],-1),ae=e("h2",{id:"scripting",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#scripting","aria-hidden":"true"},"#"),t(" Scripting")],-1),ie={href:"https://fwd.needle.tools/needle-engine/docs/npmdef",target:"_blank",rel:"noopener noreferrer"},re={href:"https://fwd.needle.tools/needle-engine/docs/component-compiler",target:"_blank",rel:"noopener noreferrer"},se=e("a",{href:"scripting"},"Scripting Reference",-1),le={href:"https://fwd.needle.tools/needle-engine/docs/npmdef",target:"_blank",rel:"noopener noreferrer"},de=e("h2",{id:"and-there-is-more",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#and-there-is-more","aria-hidden":"true"},"#"),t(" And there is more")],-1),ce=e("li",null,"PostProcessing โ Bloom, Screenspace Ambient Occlusion, Depth of Field, Color Correction...",-1),he=e("li",null,"EditorSync โ Live synchronize editing in Unity to the running three.js application for local development",-1),ue=u('
',3);function pe(me,_e){const a=l("router-link"),i=l("ExternalLinkIcon"),r=l("RouterLink"),s=l("sample");return c(),h("div",null,[m,e("nav",_,[e("ul",null,[e("li",null,[n(a,{to:"#shaders-and-materials"},{default:o(()=>[t("Shaders and Materials")]),_:1})]),e("li",null,[n(a,{to:"#crossplatform-vr-ar-mobile-desktop"},{default:o(()=>[t("Crossplatform: VR, AR, Mobile, Desktop")]),_:1})]),e("li",null,[n(a,{to:"#lightmaps"},{default:o(()=>[t("Lightmaps")]),_:1})]),e("li",null,[n(a,{to:"#multiplayer-and-networking"},{default:o(()=>[t("Multiplayer and Networking")]),_:1})]),e("li",null,[n(a,{to:"#animations-and-sequencing"},{default:o(()=>[t("Animations and Sequencing")]),_:1}),e("ul",null,[e("li",null,[n(a,{to:"#animator"},{default:o(()=>[t("Animator")]),_:1})]),e("li",null,[n(a,{to:"#timeline"},{default:o(()=>[t("Timeline")]),_:1})])])]),e("li",null,[n(a,{to:"#physics"},{default:o(()=>[t("Physics")]),_:1})]),e("li",null,[n(a,{to:"#ui"},{default:o(()=>[t("UI")]),_:1})]),e("li",null,[n(a,{to:"#particles"},{default:o(()=>[t("Particles")]),_:1})]),e("li",null,[n(a,{to:"#postprocessing"},{default:o(()=>[t("PostProcessing")]),_:1})]),e("li",null,[n(a,{to:"#editor-integrations"},{default:o(()=>[t("Editor Integrations")]),_:1})]),e("li",null,[n(a,{to:"#scripting"},{default:o(()=>[t("Scripting")]),_:1})]),e("li",null,[n(a,{to:"#and-there-is-more"},{default:o(()=>[t("And there is more")]),_:1})])])]),g,f,b,e("p",null,[t("Use the node based "),e("a",y,[t("ShaderGraph"),n(i)]),t(" to create shaders for the web. ShaderGraph makes it easy for artists to keep creating without having to worry about syntax.")]),e("p",null,[t("Read more about "),n(r,{to:"/export.html#physically-based-materials-pbr"},{default:o(()=>[t("PBR Materials")]),_:1}),t(" โข "),n(r,{to:"/export.html#custom-shaders"},{default:o(()=>[t("Custom Shaders")]),_:1})]),w,e("p",null,[t("Needle Engine runs everywhere web technology does: run the same application on desktop, mobile, AR or VR. We build Needle Engine "),n(r,{to:"/xr.html"},{default:o(()=>[t("with XR in mind")]),_:1}),t(" and consider this as and integral part of responsive webdesign!")]),e("p",null,[t("Use "),n(r,{to:"/everywhere-actions.html"},{default:o(()=>[t("Everywhere Actions")]),_:1}),t(" for "),k,t(".")]),v,x,S,e("blockquote",null,[e("p",null,[A,t(": There is no technical limitation on which lightmapper to use, as long as they end up in Unity's lightmapping data structures. Third party lightmappers such as "),e("a",E,[t("Bakery"),n(i)]),t(" thus are also supported.")])]),e("ul",null,[e("li",null,[t("Read more about "),e("a",R,[t("Exporting Lightmaps"),n(i)])])]),N,U,e("ul",null,[e("li",null,[t("Read more about "),e("a",C,[t("Networking"),n(i)]),t(" โข "),e("a",B,[t("Scripting"),n(i)])])]),T,P,e("ul",null,[e("li",null,[t("Read more about "),n(r,{to:"/component-reference.html#animation"},{default:o(()=>[t("Animation Components")]),_:1})])]),I,M,e("p",null,[t("The "),e("a",q,[t("Animator and AnimatorController"),n(i)]),t(" components in Unity let you setup animations and define conditions for when and how to blend between them. We support exporting state machines, StateMachineBehaviours, transitions and layers. StateMachineBehaviours are also supported with "),L,t(", "),O,t(" and "),V,t(" events.")]),F,j,D,e("p",null,[t("We're also translating "),e("a",G,[t("Unity's Timeline"),n(i)]),t(" setup and tracks into a web-ready format."),W,t(" Supported tracks include: AnimationTrack, AudioTrack, ActivationTrack, ControlTrack, SignalTrack.")]),z,e("blockquote",null,[e("p",null,[H,t(": It's possible to "),e("a",J,[t("export custom timeline tracks"),n(i)]),t(".")])]),e("ul",null,[e("li",null,[t("Read more about "),n(r,{to:"/component-reference.html#animation"},{default:o(()=>[t("Animation Components")]),_:1})])]),X,Y,e("ul",null,[e("li",null,[t("Read more about "),n(r,{to:"/component-reference.html#physics"},{default:o(()=>[t("Physics Components")]),_:1})])]),n(s,{src:"https://engine.needle.tools/samples-uploads/physics-animation/"}),K,Q,e("p",null,[t("See the "),n(r,{to:"/component-reference.html#ui"},{default:o(()=>[t("ui component reference")]),_:1}),t(" for supported component.")]),n(s,{src:"https://engine.needle.tools/samples-uploads/screenspace-ui"}),Z,e("p",null,[t("Export of Unity ParticleSystem (Shuriken) is in development. Features currently include world/local space simulation, box and sphere emitter shapes, emission over time as well as burst emission, velocity- and color over time, emission by velocity, texturesheet animation, basic trails."),$,t(" See a "),e("a",ee,[t("live sample"),n(i)]),t(" of supported features below:")]),n(s,{src:"https://engine.needle.tools/samples-uploads/particles/"}),te,e("p",null,[t("Builtin effects include Bloom, Screenspace Ambient Occlusion, Depth of Field, Color Correction. You can also create your own custom effects. See "),n(r,{to:"/component-reference.html#postprocessing"},{default:o(()=>[t("the component reference")]),_:1}),t(" for a complete list.")]),n(s,{src:"https://engine.needle.tools/samples-uploads/postprocessing/"}),ne,oe,ae,e("p",null,[t("Needle Engine uses as "),n(r,{to:"/scripting.html#component-architecture"},{default:o(()=>[t("component based workflow")]),_:1}),t(". Create custom scripts in typescript or javascript. Use our "),e("a",ie,[t("modular npm-based package workflow"),n(i)]),t(" integrated into Unity. A "),e("a",re,[t("typescript to C# component compiler"),n(i)]),t(" produces Unity components magically on the fly.")]),e("ul",null,[e("li",null,[t("Read more about "),se,t(" โข "),e("a",le,[t("Npm Definition Files"),n(i)])])]),de,e("ul",null,[ce,he,e("li",null,[t("Interactive AR on iOS and Android โ Use our "),n(r,{to:"/everywhere-actions.html"},{default:o(()=>[t("Everywhere Actions")]),_:1}),t(" feature set or build your own")])]),ue])}const fe=d(p,[["render",pe],["__file","features-overview.html.vue"]]);export{fe as default};
diff --git a/assets/for-unity-developers.html-32456605.js b/assets/for-unity-developers.html-32456605.js
new file mode 100644
index 000000000..0c1c97901
--- /dev/null
+++ b/assets/for-unity-developers.html-32456605.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-63ed5104","path":"/getting-started/for-unity-developers.html","title":"Scripting Introduction","lang":"en-US","frontmatter":{"title":"Scripting Introduction","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/scripting introduction.png"}],["meta",{"name":"og:description","content":"---\\nNeedle Engine provides a tight integration into the Unity Editor. This allows developers and designers alike to work together in a familiar environment and deliver fast, performant and lightweight web-experiences.\\nThe following guide is mainly aimed at developers with a Unity3D background but it may also be useful for developers with a web or three.js background. It covers topics regarding how things are done in Unity vs in three.js or Needle Engine.\\nIf you are all new to Typescript and Javascript and you want to dive into writing scripts for Needle Engine then we also recommend reading the Typescript Essentials Guide for a basic understanding between the differences between Cand Javascript/Typescript.\\nIf you want to code-along you can open engine.needle.tools/new to create a small project that you can edit in the browser โก\\nNeedle Engine is a 3d web engine running on-top of three.js. Three.js is one of the most popular 3D webgl based rendering libraries for the web. Whenever we refer to a gameObject in Needle Engine we are actually also talking about a three.js Object3D, the base type of any object in three.js. Both terms can be used interchangeably. Any gameObject is a Object3D.\\nThis also means that"}]],"description":"---\\nNeedle Engine provides a tight integration into the Unity Editor. This allows developers and designers alike to work together in a familiar environment and deliver fast, performant and lightweight web-experiences.\\nThe following guide is mainly aimed at developers with a Unity3D background but it may also be useful for developers with a web or three.js background. It covers topics regarding how things are done in Unity vs in three.js or Needle Engine.\\nIf you are all new to Typescript and Javascript and you want to dive into writing scripts for Needle Engine then we also recommend reading the Typescript Essentials Guide for a basic understanding between the differences between Cand Javascript/Typescript.\\nIf you want to code-along you can open engine.needle.tools/new to create a small project that you can edit in the browser โก\\nNeedle Engine is a 3d web engine running on-top of three.js. Three.js is one of the most popular 3D webgl based rendering libraries for the web. Whenever we refer to a gameObject in Needle Engine we are actually also talking about a three.js Object3D, the base type of any object in three.js. Both terms can be used interchangeably. Any gameObject is a Object3D.\\nThis also means that"},"headers":[{"level":2,"title":"The Basics","slug":"the-basics","link":"#the-basics","children":[]},{"level":2,"title":"Creating a Component","slug":"creating-a-component","link":"#creating-a-component","children":[]},{"level":2,"title":"Script Fields","slug":"script-fields","link":"#script-fields","children":[{"level":3,"title":"serializable","slug":"serializable","link":"#serializable","children":[]},{"level":3,"title":"public vs private","slug":"public-vs-private","link":"#public-vs-private","children":[]}]},{"level":2,"title":"GameObjects and the Scene","slug":"gameobjects-and-the-scene","link":"#gameobjects-and-the-scene","children":[]},{"level":2,"title":"Components","slug":"components","link":"#components","children":[]},{"level":2,"title":"Renamed Unity Types","slug":"renamed-unity-types","link":"#renamed-unity-types","children":[]},{"level":2,"title":"Transform","slug":"transform","link":"#transform","children":[{"level":3,"title":"WORLD- Position, Rotation, Scale...","slug":"world-position-rotation-scale...","link":"#world-position-rotation-scale...","children":[]}]},{"level":2,"title":"Time","slug":"time","link":"#time","children":[]},{"level":2,"title":"Raycasting","slug":"raycasting","link":"#raycasting","children":[]},{"level":2,"title":"Input","slug":"input","link":"#input","children":[]},{"level":2,"title":"InputSystem Callbacks","slug":"inputsystem-callbacks","link":"#inputsystem-callbacks","children":[]},{"level":2,"title":"Debug.Log","slug":"debug.log","link":"#debug.log","children":[]},{"level":2,"title":"Gizmos","slug":"gizmos","link":"#gizmos","children":[]},{"level":2,"title":"Useful Utility Methods","slug":"useful-utility-methods","link":"#useful-utility-methods","children":[]},{"level":2,"title":"The Web project","slug":"the-web-project","link":"#the-web-project","children":[]},{"level":2,"title":"Installing packages & dependencies","slug":"installing-packages-dependencies","link":"#installing-packages-dependencies","children":[{"level":3,"title":"What's the difference between 'dependencies' and 'devDependencies'","slug":"what-s-the-difference-between-dependencies-and-devdependencies","link":"#what-s-the-difference-between-dependencies-and-devdependencies","children":[]},{"level":3,"title":"How do I install another package or dependency and how to use it?","slug":"how-do-i-install-another-package-or-dependency-and-how-to-use-it","link":"#how-do-i-install-another-package-or-dependency-and-how-to-use-it","children":[]}]}],"git":{},"filePathRelative":"getting-started/for-unity-developers.md"}`);export{e as data};
diff --git a/assets/for-unity-developers.html-515345b2.js b/assets/for-unity-developers.html-515345b2.js
new file mode 100644
index 000000000..280e9d1d8
--- /dev/null
+++ b/assets/for-unity-developers.html-515345b2.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-7f349b5b","path":"/for-unity-developers.html","title":"Needle Engine for Unity Developers","lang":"en-US","frontmatter":{"title":"Needle Engine for Unity Developers","editLinks":false,"sidebar":false,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/preview.jpeg"}],["meta",{"name":"og:description","content":"---"}]],"description":"---"},"headers":[],"git":{},"filePathRelative":"for-unity-developers.md"}');export{e as data};
diff --git a/assets/for-unity-developers.html-605f2a6b.js b/assets/for-unity-developers.html-605f2a6b.js
new file mode 100644
index 000000000..ad4d44fd0
--- /dev/null
+++ b/assets/for-unity-developers.html-605f2a6b.js
@@ -0,0 +1 @@
+import{_ as t,p as n,q as o,R as e,t as r}from"./framework-c782e227.js";const a={},s=e("h1",{id:"this-page-has-been-moved-continue-here",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#this-page-has-been-moved-continue-here","aria-hidden":"true"},"#"),r(" This page has been moved: "),e("a",{href:"./getting-started/for-unity-developers"},"continue here")],-1),c=[s];function i(h,d){return n(),o("div",null,c)}const l=t(a,[["render",i],["__file","for-unity-developers.html.vue"]]);export{l as default};
diff --git a/assets/for-unity-developers.html-95a78637.js b/assets/for-unity-developers.html-95a78637.js
new file mode 100644
index 000000000..9dfd3429e
--- /dev/null
+++ b/assets/for-unity-developers.html-95a78637.js
@@ -0,0 +1,116 @@
+import{_ as l,M as c,p as r,q as d,R as e,t as n,N as s,V as i,a1 as t}from"./framework-c782e227.js";const u={},h=e("p",null,"Needle Engine provides a tight integration into the Unity Editor. This allows developers and designers alike to work together in a familiar environment and deliver fast, performant and lightweight web-experiences.",-1),m=e("p",null,"The following guide is mainly aimed at developers with a Unity3D background but it may also be useful for developers with a web or three.js background. It covers topics regarding how things are done in Unity vs in three.js or Needle Engine.",-1),k=e("p",null,[n("If you are all new to Typescript and Javascript and you want to dive into writing scripts for Needle Engine then we also recommend reading the "),e("a",{href:"./typescript-essentials"},"Typescript Essentials Guide"),n(" for a basic understanding between the differences between C# and Javascript/Typescript.")],-1),b={href:"https://engine.needle.tools/new",target:"_blank",rel:"noopener noreferrer"},v=e("h2",{id:"the-basics",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#the-basics","aria-hidden":"true"},"#"),n(" The Basics")],-1),g={href:"https://threejs.org/",target:"_blank",rel:"noopener noreferrer"},y=e("code",null,"gameObject",-1),f=e("em",null,"actually",-1),w=e("code",null,"Object3D",-1),_=e("code",null,"gameObject",-1),j=e("em",null,"is",-1),x=e("code",null,"Object3D",-1),q=e("p",null,"This also means that - if you are already familiar with three.js - you will have no problem at all using Needle Engine. Everything you can do with three.js can be done in Needle Engine as well. If you are already using certain libraries then you will be able to also use them in a Needle Engine based environment.",-1),E=e("strong",null,[n("Needle Engine's Exporter does "),e("em",null,"NOT"),n(" compile your existing C# code to Web Assembly")],-1),T=e("br",null,null,-1),O=e("em",null,"may",-1),D=e("a",{href:"./technical-overview"},"technical overview",-1),N={class:"custom-container details"},I=e("summary",null,"How to create a new Unity project with Needle Engine? (Video)",-1),U=t(`
If you have seen some Needle Engine scripts then you might have noticed that some variables are annotated with @serializable above their declaration. This is a Decorator in Typescript and can be used to modify or annotate code. In Needle Engine this is used for example to let the core serialization know which types we expect in our script when it converts from the raw component information stored in the glTF to a Component instance. Consider the following example:
This tells Needle Engine that myOtherComponent should be of type Behaviour. It will then automatically assign the correct reference to the field when your scene is loaded. The same is true for someOtherObject where we want to deserialize to an Object3D reference.
Field without any accessor modified like private, public or protected will by default be public in javascript
/// no accessor means it is public:
+myNumber?:number;
+// explicitly making it private:
+private myPrivateNumber?:number;
+protected myProtectedNumber?:number;
+
To access the current scene from a component you use this.scene which is equivalent to this.context.scene, this gives you the root three.js scene object.
To traverse the hierarchy from a component you can either iterate over the children of an object with a for loop:
for(let i =0; i <this.gameObject.children; i++)
+ const ch =this.gameObject.children[i];
+
Another option that is quite useful when you just want to iterate objects being renderable you can query all renderer components and iterate over them like so:
For getting component you can use the familiar methods similar to Unity. Note that the following uses the Animator type as an example but you can as well use any component type that is either built-in or created by you.
Method name
Desciption
this.gameObject.getComponent(Animator)
Get the Animator component on a GameObject/Object3D. It will either return the Animator instance if it has an Animator component or null if the object has no such componnent.
this.gameObject.getComponentInChildren(Animator)
Get the first Animator component on a GameObject/Object3D or on any of its children
this.gameObject.getComponentsInParents(Animator)
Get all animator components in the parent hierarchy (including the current GameObject/Object3D)
These methods are also available on the static GameObject type. For example GameObject.getComponent(this.gameObject, Animator) to get the Animator component on a passed in GameObject/Object3D.
To search the whole scene for one or multiple components you can use GameObject.findObjectOfType(Animator) or GameObject.findObjectsOfType(Animator).
Some Unity-specific types are mapped to different type names in our engine. See the following list:
Type in Unity
Type in Needle Engine
UnityEvent
EventList
A UnityEvent will be exported as a EventList type (use serializable(EventList) to deserialize UnityEvents)
GameObject
Object3D
Transform
Object3D
In three.js and Needle Engine a GameObject and a Transform are the same (there is no Transform component). The only exception to that rule is when referencing a RectTransform which is a component in Needle Engine as well.
Color
RGBAColor
The three.js color type doesnt have a alpha property. Because of that all Color types exported from Unity will be exported as RGBAColor which is a custom Needle Engine type
The major difference here to keep in mind is that position in three.js is by default a localspace position whereas in Unity position would be worldspace. The next section will explain how to get the worldspace position in three.js.
In three.js (and thus also in Needle Engine) the object.position, object.rotation, object.scale are all local space coordinates. This is different to Unity where we are used to position being worldspace and using localPosition to deliberately use the local space position.
If you want to access the world coordinates in Needle Engine we have utility methods that you can use with your objects. Call getWorldPosition(yourObject) to calculate the world position. Similar methods exist for rotation/quaternion and scale. To get access to those methods just import them from Needle Engine like so import { getWorldPosition } from "@needle.tools/engine"
Use this.context.physics.raycast() to perform a raycast and get a list of intersections. If you dont pass in any options the raycast is performed from the mouse position (or first touch position) in screenspace using the currently active mainCamera. You can also pass in a RaycastOptions object that has various settings like maxDistance, the camera to be used or the layers to be tested against.
Note that the calls above are by default raycasting against visible scene objects. That is different to Unity where you always need colliders to hit objects. The default three.js solution has both pros and cons where one major con is that it can perform quite slow depending on your scene geometry. It may be especially slow when raycasting against skinned meshes. It is therefor recommended to usually set objects with SkinnedMeshRenderers in Unity to the Ignore Raycast layer which will then be ignored by default by Needle Engine as well.
Another option is to use the physics raycast methods which will only return hits with colliders in the scene.
const hit =this.context.physics.engine?.raycast();
+
Note that in this case you have to handle all cases yourself. For example you may need to use different events if your user is visiting your website on desktop vs mobile vs a VR device. These cases are automatically handled by the Needle Engine input events (e.g. PointerDown is raised both for mouse down, touch down and in case of VR on controller button down).
The Debug.Log() equivalent in javascript is console.log(). You can also use console.warn() or console.error().
console.log("Hello web");
+// You can pass in as many arguments as you want like so:
+console.log("Hello", someVariable, GameObject.findObjectOfType(Renderer),this.context);
+
In Unity you normally have to use special methods to draw Gizmos like OnDrawGizmos or OnDrawGizmosSelected. In Needle Engine on the other hand such methods dont exist and you are free to draw gizmos from anywhere in your script. Note that it is also your responsibility then to not draw them in e.g. your deployed web application (you can just filter them by if(isDevEnvironment))).
Here is an example to draw a red wire sphere for one second for e.g. visualizing a point in worldspace
Import from @needle-tools/engine e.g. import { getParam } from "@needle-tools/engine"
Method name
Description
getParam()
Checks if a url parameter exists. Returns true if it exists but has no value (e.g. ?help), false if it is not found in the url or is set to 0 (e.g. ?help=0), otherwise it returns the value (e.g. ?message=test)
isMobileDevice()
Returns true if the app is accessed from a mobile device
isDevEnvironment()
Returns true if the current app is running on a local server
In C# you usually work with a solution containing one or many projects. In Unity this solution is managed by Unity for you and when you open a C# script it opens the project and shows you the file. You usually install Packages using Unity's built-in package manager to add features provided by either Unity or other developers (either on your team or e.g. via Unity's AssetStore). Unity does a great job of making adding and managing packages easy with their PackageManager and you might never have had to manually edit a file like the manifest.json (this is what Unity uses to track which packages are installed) or run a command from the command line to install a package.
In a web environment you use npm - the Node Package Manager - to manage dependencies / packages for you. It does basically the same to what Unity's PackageManager does - it installs (downloads) packages from some server (you hear it usually called a registry in that context) and puts them inside a folder named node_modules.
Our default template uses Vite as its bundler and has no frontend framework pre-installed. Needle Engine is unoppionated about which framework to use so you are free to work with whatever framework you like. We have samples for popular frameworks like Vue.js, Svelte, Next.js, React or React Three Fiber.
`,4),Tn=e("code",null,"npm i ",-1),On=e("code",null,"npm install",-1),Dn=e("br",null,null,-1),Nn=e("code",null,"npm i @needle-tools/engine",-1),In={href:"https://www.npmjs.com/package/@needle-tools/engine",target:"_blank",rel:"noopener noreferrer"},Un=e("code",null,"package.json",-1),Cn=e("code",null,"dependencies",-1),Pn=e("br",null,null,-1),Rn=e("code",null,"npm i --save-dev ",-1),Gn=t('
# What's the difference between 'dependencies' and 'devDependencies'
You may have noticed that there are two entries containing dependency - dependencies and devDependencies.
dependencies are always installed (or bundled) when either your web project is installed or in cases where you develop a library and your package is installed as a dependency of another project.
devDependencies are only installed when developing the project (meaning that when you directly run install in the specific directory) and they are otherwise not included in your project.
# How do I install another package or dependency and how to use it?
',5),zn=e("a",{href:"#installing"},"Installing",-1),Sn=e("code",null,"npm i ",-1),An=e("code",null,"package_name",-1),Wn={href:"https://npmjs.org",target:"_blank",rel:"noopener noreferrer"},Bn={href:"https://www.npmjs.com/package/@tweenjs/tween.js",target:"_blank",rel:"noopener noreferrer"},Mn=e("code",null,"@tweenjs/tween.js",-1),Fn={href:"https://stackblitz.com/edit/needle-engine-tweenjs-example?file=src%2Fmain.ts",target:"_blank",rel:"noopener noreferrer"},Ln=t(`
First run npm install @tweenjs/tween.js in the terminal and wait for the installation to finish. This will add a new entry to our package.json:
Then open one of your script files in which you want to use tweening and import at the top of the file:
import*asTWEENfrom'@tweenjs/tween.js';
+
Note that we do here import all types in the library by writing * as TWEEN. We could also just import specific types like import { Tween } from @tweenjs/tween.js.
To rotate a cube we create a new component type called TweenRotation, we then go ahead and create our tween instance for the object rotation, how often it should repeat, which easing to use, the tween we want to perform and then we start it. We then only have to call update every frame to update the tween animation. The final script looks like this:
exportclassTweenRotationextendsBehaviour{
+
+ // save the instance of our tweener
+ private _tween?:TWEEN.Tween<any>;
+
+ start(){
+ // create the tween instance
+ this._tween =newTWEEN.Tween(this.gameObject.rotation);
+ // set it to repeat forever
+ this._tween.repeat(Infinity);
+ // set the easing to use
+ this._tween.easing(TWEEN.Easing.Quintic.InOut);
+ // set the values to tween
+ this._tween.to({ y: Math.PI*0.5},1000);
+ // start it
+ this._tween.start();
+ }
+
+ update(){
+ // update the tweening every frame
+ // the '?' is a shorthand for checking if _tween has been created
+ this._tween?.update();
+ }
+}
+
`,2),Yn=e("br",null,null,-1),Jn={href:"https://stackblitz.com/edit/needle-engine-tweenjs-example?file=src%2Fmain.ts",target:"_blank",rel:"noopener noreferrer"},Xn=e("h1",{id:"learning-more",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#learning-more","aria-hidden":"true"},"#"),n(" Learning more")],-1),Kn=e("li",null,[e("a",{href:"../scripting"},"Scripting in Needle Engine")],-1);function Qn(Zn,$n){const a=c("ExternalLinkIcon"),o=c("RouterLink"),p=c("video-embed");return r(),d("div",null,[h,m,k,e("p",null,[n("If you want to code-along you can "),e("a",b,[n("open engine.needle.tools/new"),s(a)]),n(" to create a small project that you can edit in the browser โก")]),v,e("p",null,[n("Needle Engine is a 3d web engine running on-top of "),e("a",g,[n("three.js"),s(a)]),n(". Three.js is one of the most popular 3D webgl based rendering libraries for the web. Whenever we refer to a "),y,n(" in Needle Engine we are "),f,n(" also talking about a three.js "),w,n(", the base type of any object in three.js. Both terms can be used interchangeably. Any "),_,n(),j,n(" a "),x,n(".")]),q,e("p",null,[n("Note: "),E,n("."),T,n(" While using Web Assembly "),O,n(" result in better performance at runtime, it comes at a high cost for iteration speed and flexibility in building web experiences. Read more about our "),s(o,{to:"/getting-started/vision.html"},{default:i(()=>[n("vision")]),_:1}),n(" and "),D,n(".")]),e("details",N,[I,s(p,{src:"https://www.youtube.com/watch?v=gZX_sqrne8U",limit_height:""})]),U,e("p",null,[n("Note that in some cases the type can be ommitted. This can be done for all "),e("a",C,[n("primitive types in Javascript"),s(a)]),n(". These are "),P,n(", "),R,n(", "),G,n(", "),z,n(", "),S,n(" and "),A,n(".")]),W,e("p",null,[n("You can also use three.js specific methods to quickly iterate all objects recursively using the "),e("a",B,[M,s(a)]),n(" method:")]),F,e("p",null,[n("or to just traverse visible objects use "),e("a",L,[H,s(a)]),n(" instead.")]),V,e("p",null,[n("Needle Engine is making heavy use of a Component System that is similar to that of Unity. This means that you can add or remove components to any "),Y,n(" / "),J,n(" in the scene. A component will be registered to the engine when using "),X,n("."),K,n(" The event methods that the attached component will then automatically be called by the engine (e.g. "),Q,n(" or "),Z,n("). A full list of event methods can be found in the "),s(o,{to:"/scripting.html#lifecycle-methods"},{default:i(()=>[n("scripting documentation")]),_:1})]),$,e("ul",null,[e("li",null,[nn,n(" is the "),e("a",en,[n("position"),s(a)]),n(" in local space")]),e("li",null,[sn,n(" is the "),e("a",an,[n("rotation in euler angles"),s(a)]),n(" in local space")]),e("li",null,[tn,n(" - is the "),e("a",on,[n("quaternion"),s(a)]),n(" in local space")]),e("li",null,[cn,n(" - is the "),e("a",pn,[n("scale"),s(a)]),n(" in local space")])]),ln,e("p",null,[n("Note that these utility methods like "),rn,n(", "),dn,n(", "),un,n(" internally have a buffer of Vector3 instances and are meant to be used locally only. This means that you should not cache them in your component, otherwise your cached value will eventually be overriden. But it is safe to call "),hn,n(" multiple times in your function to make calculations without having to worry to re-use the same instance. If you are not sure what this means you should take a look at the "),mn,n(" section in the "),s(o,{to:"/getting-started/typescript-essentials.html#primitive-types"},{default:i(()=>[n("Typescript Essentials Guide")]),_:1})]),kn,e("p",null,[n("Use "),bn,n(" to perform a raycast using a "),e("a",vn,[n("three.js ray"),s(a)])]),gn,e("p",null,[n("Here is a editable "),e("a",yn,[n("example for physics raycast"),s(a)])]),fn,e("p",null,[n("If you want to handle inputs yourself you can also subscribe to "),e("a",wn,[n("all events the browser provides"),s(a)]),n(" (there are a ton). For example to subscribe to the browsers click event you can write:")]),_n,e("p",null,[n("Similar to Unity (see "),e("a",jn,[n("IPointerClickHandler in Unity"),s(a)]),n(") you can also register to receive input events on the component itself.")]),xn,e("p",null,[n("When working with a web project most of you dependencies are installed from "),e("a",qn,[n("npmjs.com"),s(a)]),n(". It is the most popular package registry out there for web projects.")]),En,e("p",null,[n("To install a dependency from npm you can open your web project in a commandline (or terminal) and run "),Tn,n(" (shorthand for "),On,n(")"),Dn,n(" For example run "),Nn,n(" to install "),e("a",In,[n("Needle Engine"),s(a)]),n(". This will then add the package to your "),Un,n(" to the "),Cn,n(" array."),Pn,n(" To install a package as a devDependency only you can run "),Rn,n(". More about the difference between dependencies and devDependencies below.")]),Gn,e("p",null,[n("The "),zn,n(" section taught us that you can install dependencies by running "),Sn,n(" in your project directory where the "),An,n(" can be any package that you find on "),e("a",Wn,[n("npm.js"),s(a)]),n(".")]),e("p",null,[n("Let's assume you want to add a tweening library to your project. We will use "),e("a",Bn,[Mn,s(a)]),n(" for this example. "),e("a",Fn,[n("Here"),s(a)]),n(" is the final project if you want to jump ahead and just see the result.")]),Ln,e("p",null,[n("Now we can use it in our script. It is always recommended to refer to the documentation of the library that you want to use. In the case of tween.js they provide a "),e("a",Hn,[n("user guide"),s(a)]),n(" that we can follow. Usually the Readme page of the package on npm contains information on how to install and use the package.")]),Vn,e("p",null,[n("Now we only have to add it to any of the objects in our scene to rotate them forever."),Yn,n(" You can see the final script in action "),e("a",Jn,[n("here"),s(a)]),n(".")]),Xn,e("ul",null,[Kn,e("li",null,[s(o,{to:"/getting-started/typescript-essentials.html"},{default:i(()=>[n("Typescript Essentials")]),_:1})]),e("li",null,[s(o,{to:"/component-reference.html"},{default:i(()=>[n("Component Reference")]),_:1})])])])}const ee=l(u,[["render",Qn],["__file","for-unity-developers.html.vue"]]);export{ee as default};
diff --git a/assets/framework-c782e227.js b/assets/framework-c782e227.js
new file mode 100644
index 000000000..120100bbf
--- /dev/null
+++ b/assets/framework-c782e227.js
@@ -0,0 +1,21 @@
+/**
+* @vue/shared v3.4.21
+* (c) 2018-present Yuxi (Evan) You and Vue contributors
+* @license MIT
+**/function Cs(e,t){const n=new Set(e.split(","));return t?s=>n.has(s.toLowerCase()):s=>n.has(s)}const ie={},xt=[],Ae=()=>{},ho=()=>!1,Zt=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),xs=e=>e.startsWith("onUpdate:"),pe=Object.assign,ws=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},po=Object.prototype.hasOwnProperty,ee=(e,t)=>po.call(e,t),q=Array.isArray,wt=e=>Mn(e)==="[object Map]",Gr=e=>Mn(e)==="[object Set]",J=e=>typeof e=="function",he=e=>typeof e=="string",Ft=e=>typeof e=="symbol",oe=e=>e!==null&&typeof e=="object",zr=e=>(oe(e)||J(e))&&J(e.then)&&J(e.catch),Qr=Object.prototype.toString,Mn=e=>Qr.call(e),go=e=>Mn(e).slice(8,-1),Jr=e=>Mn(e)==="[object Object]",Ss=e=>he(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,St=Cs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),In=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},mo=/-(\w)/g,Ue=In(e=>e.replace(mo,(t,n)=>n?n.toUpperCase():"")),yo=/\B([A-Z])/g,mt=In(e=>e.replace(yo,"-$1").toLowerCase()),Ln=In(e=>e.charAt(0).toUpperCase()+e.slice(1)),Wn=In(e=>e?`on${Ln(e)}`:""),ot=(e,t)=>!Object.is(e,t),qn=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},_o=e=>{const t=parseFloat(e);return isNaN(t)?e:t},bo=e=>{const t=he(e)?Number(e):NaN;return isNaN(t)?e:t};let zs;const Yr=()=>zs||(zs=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Rs(e){if(q(e)){const t={};for(let n=0;n{if(n){const s=n.split(Eo);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function As(e){let t="";if(he(e))t=e;else if(q(e))for(let n=0;nhe(e)?e:e==null?"":q(e)||oe(e)&&(e.toString===Qr||!J(e.toString))?JSON.stringify(e,Zr,2):String(e),Zr=(e,t)=>t&&t.__v_isRef?Zr(e,t.value):wt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[Gn(s,i)+" =>"]=r,n),{})}:Gr(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Gn(n))}:Ft(t)?Gn(t):oe(t)&&!q(t)&&!Jr(t)?String(t):t,Gn=(e,t="")=>{var n;return Ft(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/**
+* @vue/reactivity v3.4.21
+* (c) 2018-present Yuxi (Evan) You and Vue contributors
+* @license MIT
+**/let Re;class Ro{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=Re,!t&&Re&&(this.index=(Re.scopes||(Re.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=Re;try{return Re=this,t()}finally{Re=n}}}on(){Re=this}off(){Re=this.parent}stop(t){if(this._active){let n,s;for(n=0,s=this.effects.length;n=4))break}this._dirtyLevel===1&&(this._dirtyLevel=0),_t()}return this._dirtyLevel>=4}set dirty(t){this._dirtyLevel=t?4:0}run(){if(this._dirtyLevel=0,!this.active)return this.fn();let t=rt,n=pt;try{return rt=!0,pt=this,this._runnings++,Qs(this),this.fn()}finally{Js(this),this._runnings--,pt=n,rt=t}}stop(){var t;this.active&&(Qs(this),Js(this),(t=this.onStop)==null||t.call(this),this.active=!1)}}function To(e){return e.value}function Qs(e){e._trackId++,e._depsLength=0}function Js(e){if(e.deps.length>e._depsLength){for(let t=e._depsLength;t{const n=new Map;return n.cleanup=e,n.computed=t,n},Cn=new WeakMap,gt=Symbol(""),cs=Symbol("");function we(e,t,n){if(rt&&pt){let s=Cn.get(e);s||Cn.set(e,s=new Map);let r=s.get(n);r||s.set(n,r=ri(()=>s.delete(n))),ni(pt,r)}}function We(e,t,n,s,r,i){const o=Cn.get(e);if(!o)return;let c=[];if(t==="clear")c=[...o.values()];else if(n==="length"&&q(e)){const l=Number(s);o.forEach((h,f)=>{(f==="length"||!Ft(f)&&f>=l)&&c.push(h)})}else switch(n!==void 0&&c.push(o.get(n)),t){case"add":q(e)?Ss(n)&&c.push(o.get("length")):(c.push(o.get(gt)),wt(e)&&c.push(o.get(cs)));break;case"delete":q(e)||(c.push(o.get(gt)),wt(e)&&c.push(o.get(cs)));break;case"set":wt(e)&&c.push(o.get(gt));break}Ts();for(const l of c)l&&si(l,4);Os()}function Oo(e,t){var n;return(n=Cn.get(e))==null?void 0:n.get(t)}const Mo=Cs("__proto__,__v_isRef,__isVue"),ii=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Ft)),Ys=Io();function Io(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const s=te(this);for(let i=0,o=this.length;i{e[t]=function(...n){yt(),Ts();const s=te(this)[t].apply(this,n);return Os(),_t(),s}}),e}function Lo(e){const t=te(this);return we(t,"has",e),t.hasOwnProperty(e)}class oi{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?qo:fi:i?ui:ci).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=q(t);if(!r){if(o&&ee(Ys,n))return Reflect.get(Ys,n,s);if(n==="hasOwnProperty")return Lo}const c=Reflect.get(t,n,s);return(Ft(n)?ii.has(n):Mo(n))||(r||we(t,"get",n),i)?c:Ee(c)?o&&Ss(n)?c:c.value:oe(c)?r?hi(c):Fn(c):c}}class li extends oi{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];if(!this._isShallow){const l=Mt(i);if(!xn(s)&&!Mt(s)&&(i=te(i),s=te(s)),!q(t)&&Ee(i)&&!Ee(s))return l?!1:(i.value=s,!0)}const o=q(t)&&Ss(n)?Number(n)e,Nn=e=>Reflect.getPrototypeOf(e);function on(e,t,n=!1,s=!1){e=e.__v_raw;const r=te(e),i=te(t);n||(ot(t,i)&&we(r,"get",t),we(r,"get",i));const{has:o}=Nn(r),c=s?Ms:n?Ns:qt;if(o.call(r,t))return c(e.get(t));if(o.call(r,i))return c(e.get(i));e!==r&&e.get(t)}function ln(e,t=!1){const n=this.__v_raw,s=te(n),r=te(e);return t||(ot(e,r)&&we(s,"has",e),we(s,"has",r)),e===r?n.has(e):n.has(e)||n.has(r)}function cn(e,t=!1){return e=e.__v_raw,!t&&we(te(e),"iterate",gt),Reflect.get(e,"size",e)}function Xs(e){e=te(e);const t=te(this);return Nn(t).has.call(t,e)||(t.add(e),We(t,"add",e,e)),this}function Zs(e,t){t=te(t);const n=te(this),{has:s,get:r}=Nn(n);let i=s.call(n,e);i||(e=te(e),i=s.call(n,e));const o=r.call(n,e);return n.set(e,t),i?ot(t,o)&&We(n,"set",e,t):We(n,"add",e,t),this}function er(e){const t=te(this),{has:n,get:s}=Nn(t);let r=n.call(t,e);r||(e=te(e),r=n.call(t,e)),s&&s.call(t,e);const i=t.delete(e);return r&&We(t,"delete",e,void 0),i}function tr(){const e=te(this),t=e.size!==0,n=e.clear();return t&&We(e,"clear",void 0,void 0),n}function un(e,t){return function(s,r){const i=this,o=i.__v_raw,c=te(o),l=t?Ms:e?Ns:qt;return!e&&we(c,"iterate",gt),o.forEach((h,f)=>s.call(r,l(h),l(f),i))}}function fn(e,t,n){return function(...s){const r=this.__v_raw,i=te(r),o=wt(i),c=e==="entries"||e===Symbol.iterator&&o,l=e==="keys"&&o,h=r[e](...s),f=n?Ms:t?Ns:qt;return!t&&we(i,"iterate",l?cs:gt),{next(){const{value:d,done:p}=h.next();return p?{value:d,done:p}:{value:c?[f(d[0]),f(d[1])]:f(d),done:p}},[Symbol.iterator](){return this}}}}function ze(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function ko(){const e={get(i){return on(this,i)},get size(){return cn(this)},has:ln,add:Xs,set:Zs,delete:er,clear:tr,forEach:un(!1,!1)},t={get(i){return on(this,i,!1,!0)},get size(){return cn(this)},has:ln,add:Xs,set:Zs,delete:er,clear:tr,forEach:un(!1,!0)},n={get(i){return on(this,i,!0)},get size(){return cn(this,!0)},has(i){return ln.call(this,i,!0)},add:ze("add"),set:ze("set"),delete:ze("delete"),clear:ze("clear"),forEach:un(!0,!1)},s={get(i){return on(this,i,!0,!0)},get size(){return cn(this,!0)},has(i){return ln.call(this,i,!0)},add:ze("add"),set:ze("set"),delete:ze("delete"),clear:ze("clear"),forEach:un(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(i=>{e[i]=fn(i,!1,!1),n[i]=fn(i,!0,!1),t[i]=fn(i,!1,!0),s[i]=fn(i,!0,!0)}),[e,n,t,s]}const[jo,Bo,Vo,Do]=ko();function Is(e,t){const n=t?e?Do:Vo:e?Bo:jo;return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(ee(n,r)&&r in s?n:s,r,i)}const Uo={get:Is(!1,!1)},Ko={get:Is(!1,!0)},Wo={get:Is(!0,!1)},ci=new WeakMap,ui=new WeakMap,fi=new WeakMap,qo=new WeakMap;function Go(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function zo(e){return e.__v_skip||!Object.isExtensible(e)?0:Go(go(e))}function Fn(e){return Mt(e)?e:Ls(e,!1,Fo,Uo,ci)}function ai(e){return Ls(e,!1,Ho,Ko,ui)}function hi(e){return Ls(e,!0,$o,Wo,fi)}function Ls(e,t,n,s,r){if(!oe(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=r.get(e);if(i)return i;const o=zo(e);if(o===0)return e;const c=new Proxy(e,o===2?s:n);return r.set(e,c),c}function Rt(e){return Mt(e)?Rt(e.__v_raw):!!(e&&e.__v_isReactive)}function Mt(e){return!!(e&&e.__v_isReadonly)}function xn(e){return!!(e&&e.__v_isShallow)}function di(e){return Rt(e)||Mt(e)}function te(e){const t=e&&e.__v_raw;return t?te(t):e}function pi(e){return Object.isExtensible(e)&&En(e,"__v_skip",!0),e}const qt=e=>oe(e)?Fn(e):e,Ns=e=>oe(e)?hi(e):e;class gi{constructor(t,n,s,r){this.getter=t,this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this.effect=new Ps(()=>t(this._value),()=>gn(this,this.effect._dirtyLevel===2?2:3)),this.effect.computed=this,this.effect.active=this._cacheable=!r,this.__v_isReadonly=s}get value(){const t=te(this);return(!t._cacheable||t.effect.dirty)&&ot(t._value,t._value=t.effect.run())&&gn(t,4),mi(t),t.effect._dirtyLevel>=2&&gn(t,2),t._value}set value(t){this._setter(t)}get _dirty(){return this.effect.dirty}set _dirty(t){this.effect.dirty=t}}function Qo(e,t,n=!1){let s,r;const i=J(e);return i?(s=e,r=Ae):(s=e.get,r=e.set),new gi(s,r,i||!r,n)}function mi(e){var t;rt&&pt&&(e=te(e),ni(pt,(t=e.dep)!=null?t:e.dep=ri(()=>e.dep=void 0,e instanceof gi?e:void 0)))}function gn(e,t=4,n){e=te(e);const s=e.dep;s&&si(s,t)}function Ee(e){return!!(e&&e.__v_isRef===!0)}function mn(e){return yi(e,!1)}function Jo(e){return yi(e,!0)}function yi(e,t){return Ee(e)?e:new Yo(e,t)}class Yo{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:te(t),this._value=n?t:qt(t)}get value(){return mi(this),this._value}set value(t){const n=this.__v_isShallow||xn(t)||Mt(t);t=n?t:te(t),ot(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:qt(t),gn(this,4))}}function At(e){return Ee(e)?e.value:e}const Xo={get:(e,t,n)=>At(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return Ee(r)&&!Ee(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function _i(e){return Rt(e)?e:new Proxy(e,Xo)}function Wu(e){const t=q(e)?new Array(e.length):{};for(const n in e)t[n]=el(e,n);return t}class Zo{constructor(t,n,s){this._object=t,this._key=n,this._defaultValue=s,this.__v_isRef=!0}get value(){const t=this._object[this._key];return t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return Oo(te(this._object),this._key)}}function el(e,t,n){const s=e[t];return Ee(s)?s:new Zo(e,t,n)}/**
+* @vue/runtime-core v3.4.21
+* (c) 2018-present Yuxi (Evan) You and Vue contributors
+* @license MIT
+**/function it(e,t,n,s){try{return s?e(...s):e()}catch(r){en(r,t,n)}}function Me(e,t,n,s){if(J(e)){const i=it(e,t,n,s);return i&&zr(i)&&i.catch(o=>{en(o,t,n)}),i}const r=[];for(let i=0;i>>1,r=_e[s],i=zt(r);iDe&&_e.splice(t,1)}function rl(e){q(e)?Pt.push(...e):(!Ze||!Ze.includes(e,e.allowRecurse?at+1:at))&&Pt.push(e),Ei()}function nr(e,t,n=Gt?De+1:0){for(;n<_e.length;n++){const s=_e[n];if(s&&s.pre){if(e&&s.id!==e.uid)continue;_e.splice(n,1),n--,s()}}}function wn(e){if(Pt.length){const t=[...new Set(Pt)].sort((n,s)=>zt(n)-zt(s));if(Pt.length=0,Ze){Ze.push(...t);return}for(Ze=t,at=0;ate.id==null?1/0:e.id,il=(e,t)=>{const n=zt(e)-zt(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Ci(e){us=!1,Gt=!0,_e.sort(il);const t=Ae;try{for(De=0;De<_e.length;De++){const n=_e[De];n&&n.active!==!1&&it(n,null,14)}}finally{De=0,_e.length=0,wn(),Gt=!1,Fs=null,(_e.length||Pt.length)&&Ci()}}function ol(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||ie;let r=n;const i=t.startsWith("update:"),o=i&&t.slice(7);if(o&&o in s){const f=`${o==="modelValue"?"model":o}Modifiers`,{number:d,trim:p}=s[f]||ie;p&&(r=n.map(y=>he(y)?y.trim():y)),d&&(r=n.map(_o))}let c,l=s[c=Wn(t)]||s[c=Wn(Ue(t))];!l&&i&&(l=s[c=Wn(mt(t))]),l&&Me(l,e,6,r);const h=s[c+"Once"];if(h){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,Me(h,e,6,r)}}function xi(e,t,n=!1){const s=t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},c=!1;if(!J(e)){const l=h=>{const f=xi(h,t,!0);f&&(c=!0,pe(o,f))};!n&&t.mixins.length&&t.mixins.forEach(l),e.extends&&l(e.extends),e.mixins&&e.mixins.forEach(l)}return!i&&!c?(oe(e)&&s.set(e,null),null):(q(i)?i.forEach(l=>o[l]=null):pe(o,i),oe(e)&&s.set(e,o),o)}function Hn(e,t){return!e||!Zt(t)?!1:(t=t.slice(2).replace(/Once$/,""),ee(e,t[0].toLowerCase()+t.slice(1))||ee(e,mt(t))||ee(e,t))}let de=null,kn=null;function Sn(e){const t=de;return de=e,kn=e&&e.type.__scopeId||null,t}function qu(e){kn=e}function Gu(){kn=null}function ll(e,t=de,n){if(!t||e._n)return e;const s=(...r)=>{s._d&&pr(-1);const i=Sn(t);let o;try{o=e(...r)}finally{Sn(i),s._d&&pr(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function zn(e){const{type:t,vnode:n,proxy:s,withProxy:r,props:i,propsOptions:[o],slots:c,attrs:l,emit:h,render:f,renderCache:d,data:p,setupState:y,ctx:x,inheritAttrs:F}=e;let k,I;const M=Sn(e);try{if(n.shapeFlag&4){const E=r||s,$=E;k=Ne(f.call($,E,d,i,y,p,x)),I=l}else{const E=t;k=Ne(E.length>1?E(i,{attrs:l,slots:c,emit:h}):E(i,null)),I=t.props?l:cl(l)}}catch(E){Ut.length=0,en(E,e,1),k=ae(Pe)}let m=k;if(I&&F!==!1){const E=Object.keys(I),{shapeFlag:$}=m;E.length&&$&7&&(o&&E.some(xs)&&(I=ul(I,o)),m=lt(m,I))}return n.dirs&&(m=lt(m),m.dirs=m.dirs?m.dirs.concat(n.dirs):n.dirs),n.transition&&(m.transition=n.transition),k=m,Sn(M),k}const cl=e=>{let t;for(const n in e)(n==="class"||n==="style"||Zt(n))&&((t||(t={}))[n]=e[n]);return t},ul=(e,t)=>{const n={};for(const s in e)(!xs(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function fl(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:c,patchFlag:l}=t,h=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&l>=0){if(l&1024)return!0;if(l&16)return s?sr(s,o,h):!!o;if(l&8){const f=t.dynamicProps;for(let d=0;de.__isSuspense;function Si(e,t){t&&t.pendingBranch?q(e)?t.effects.push(...e):t.effects.push(e):rl(e)}const gl=Symbol.for("v-scx"),ml=()=>He(gl);function Qu(e,t){return jn(e,null,t)}function yl(e,t){return jn(e,null,{flush:"post"})}const an={};function yn(e,t,n){return jn(e,t,n)}function jn(e,t,{immediate:n,deep:s,flush:r,once:i,onTrack:o,onTrigger:c}=ie){if(t&&i){const P=t;t=(...G)=>{P(...G),$()}}const l=me,h=P=>s===!0?P:dt(P,s===!1?1:void 0);let f,d=!1,p=!1;if(Ee(e)?(f=()=>e.value,d=xn(e)):Rt(e)?(f=()=>h(e),d=!0):q(e)?(p=!0,d=e.some(P=>Rt(P)||xn(P)),f=()=>e.map(P=>{if(Ee(P))return P.value;if(Rt(P))return h(P);if(J(P))return it(P,l,2)})):J(e)?t?f=()=>it(e,l,2):f=()=>(y&&y(),Me(e,l,3,[x])):f=Ae,t&&s){const P=f;f=()=>dt(P())}let y,x=P=>{y=m.onStop=()=>{it(P,l,4),y=m.onStop=void 0}},F;if(sn)if(x=Ae,t?n&&Me(t,l,3,[f(),p?[]:void 0,x]):f(),r==="sync"){const P=ml();F=P.__watcherHandles||(P.__watcherHandles=[])}else return Ae;let k=p?new Array(e.length).fill(an):an;const I=()=>{if(!(!m.active||!m.dirty))if(t){const P=m.run();(s||d||(p?P.some((G,H)=>ot(G,k[H])):ot(P,k)))&&(y&&y(),Me(t,l,3,[P,k===an?void 0:p&&k[0]===an?[]:k,x]),k=P)}else m.run()};I.allowRecurse=!!t;let M;r==="sync"?M=I:r==="post"?M=()=>xe(I,l&&l.suspense):(I.pre=!0,l&&(I.id=l.uid),M=()=>$n(I));const m=new Ps(f,Ae,M),E=Po(),$=()=>{m.stop(),E&&ws(E.effects,m)};return t?n?I():k=m.run():r==="post"?xe(m.run.bind(m),l&&l.suspense):m.run(),F&&F.push($),$}function _l(e,t,n){const s=this.proxy,r=he(e)?e.includes(".")?Ri(s,e):()=>s[e]:e.bind(s,s);let i;J(t)?i=t:(i=t.handler,n=t);const o=nn(this),c=jn(r,i.bind(s),n);return o(),c}function Ri(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;r0){if(n>=t)return e;n++}if(s=s||new Set,s.has(e))return e;if(s.add(e),Ee(e))dt(e.value,t,n,s);else if(q(e))for(let r=0;r{dt(r,t,n,s)});else if(Jr(e))for(const r in e)dt(e[r],t,n,s);return e}function Ju(e,t){if(de===null)return e;const n=Dn(de)||de.proxy,s=e.dirs||(e.dirs=[]);for(let r=0;r{e.isMounted=!0}),Mi(()=>{e.isUnmounting=!0}),e}const Te=[Function,Array],Ai={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Te,onEnter:Te,onAfterEnter:Te,onEnterCancelled:Te,onBeforeLeave:Te,onLeave:Te,onAfterLeave:Te,onLeaveCancelled:Te,onBeforeAppear:Te,onAppear:Te,onAfterAppear:Te,onAppearCancelled:Te},vl={name:"BaseTransition",props:Ai,setup(e,{slots:t}){const n=Qi(),s=bl();return()=>{const r=t.default&&Ti(t.default(),!0);if(!r||!r.length)return;let i=r[0];if(r.length>1){for(const p of r)if(p.type!==Pe){i=p;break}}const o=te(e),{mode:c}=o;if(s.isLeaving)return Qn(i);const l=ir(i);if(!l)return Qn(i);const h=fs(l,o,s,n);as(l,h);const f=n.subTree,d=f&&ir(f);if(d&&d.type!==Pe&&!ht(l,d)){const p=fs(d,o,s,n);if(as(d,p),c==="out-in")return s.isLeaving=!0,p.afterLeave=()=>{s.isLeaving=!1,n.update.active!==!1&&(n.effect.dirty=!0,n.update())},Qn(i);c==="in-out"&&l.type!==Pe&&(p.delayLeave=(y,x,F)=>{const k=Pi(s,d);k[String(d.key)]=d,y[et]=()=>{x(),y[et]=void 0,delete h.delayedLeave},h.delayedLeave=F})}return i}}},El=vl;function Pi(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function fs(e,t,n,s){const{appear:r,mode:i,persisted:o=!1,onBeforeEnter:c,onEnter:l,onAfterEnter:h,onEnterCancelled:f,onBeforeLeave:d,onLeave:p,onAfterLeave:y,onLeaveCancelled:x,onBeforeAppear:F,onAppear:k,onAfterAppear:I,onAppearCancelled:M}=t,m=String(e.key),E=Pi(n,e),$=(H,W)=>{H&&Me(H,s,9,W)},P=(H,W)=>{const j=W[1];$(H,W),q(H)?H.every(Z=>Z.length<=1)&&j():H.length<=1&&j()},G={mode:i,persisted:o,beforeEnter(H){let W=c;if(!n.isMounted)if(r)W=F||c;else return;H[et]&&H[et](!0);const j=E[m];j&&ht(e,j)&&j.el[et]&&j.el[et](),$(W,[H])},enter(H){let W=l,j=h,Z=f;if(!n.isMounted)if(r)W=k||l,j=I||h,Z=M||f;else return;let A=!1;const z=H[hn]=ue=>{A||(A=!0,ue?$(Z,[H]):$(j,[H]),G.delayedLeave&&G.delayedLeave(),H[hn]=void 0)};W?P(W,[H,z]):z()},leave(H,W){const j=String(e.key);if(H[hn]&&H[hn](!0),n.isUnmounting)return W();$(d,[H]);let Z=!1;const A=H[et]=z=>{Z||(Z=!0,W(),z?$(x,[H]):$(y,[H]),H[et]=void 0,E[j]===e&&delete E[j])};E[j]=e,p?P(p,[H,A]):A()},clone(H){return fs(H,t,n,s)}};return G}function Qn(e){if(tn(e))return e=lt(e),e.children=null,e}function ir(e){return tn(e)?e.children?e.children[0]:void 0:e}function as(e,t){e.shapeFlag&6&&e.component?as(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Ti(e,t=!1,n){let s=[],r=0;for(let i=0;i1)for(let i=0;ipe({name:e.name},t,{setup:e}))():e}const Tt=e=>!!e.type.__asyncLoader;/*! #__NO_SIDE_EFFECTS__ */function Yu(e){J(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:s,delay:r=200,timeout:i,suspensible:o=!0,onError:c}=e;let l=null,h,f=0;const d=()=>(f++,l=null,p()),p=()=>{let y;return l||(y=l=t().catch(x=>{if(x=x instanceof Error?x:new Error(String(x)),c)return new Promise((F,k)=>{c(x,()=>F(d()),()=>k(x),f+1)});throw x}).then(x=>y!==l&&l?l:(x&&(x.__esModule||x[Symbol.toStringTag]==="Module")&&(x=x.default),h=x,x)))};return $s({name:"AsyncComponentWrapper",__asyncLoader:p,get __asyncResolved(){return h},setup(){const y=me;if(h)return()=>Jn(h,y);const x=M=>{l=null,en(M,y,13,!s)};if(o&&y.suspense||sn)return p().then(M=>()=>Jn(M,y)).catch(M=>(x(M),()=>s?ae(s,{error:M}):null));const F=mn(!1),k=mn(),I=mn(!!r);return r&&setTimeout(()=>{I.value=!1},r),i!=null&&setTimeout(()=>{if(!F.value&&!k.value){const M=new Error(`Async component timed out after ${i}ms.`);x(M),k.value=M}},i),p().then(()=>{F.value=!0,y.parent&&tn(y.parent.vnode)&&(y.parent.effect.dirty=!0,$n(y.parent.update))}).catch(M=>{x(M),k.value=M}),()=>{if(F.value&&h)return Jn(h,y);if(k.value&&s)return ae(s,{error:k.value});if(n&&!I.value)return ae(n)}}})}function Jn(e,t){const{ref:n,props:s,children:r,ce:i}=t.vnode,o=ae(e,s,r);return o.ref=n,o.ce=i,delete t.vnode.ce,o}const tn=e=>e.type.__isKeepAlive;function Cl(e,t){Oi(e,"a",t)}function xl(e,t){Oi(e,"da",t)}function Oi(e,t,n=me){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Bn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)tn(r.parent.vnode)&&wl(s,t,n,r),r=r.parent}}function wl(e,t,n,s){const r=Bn(t,e,s,!0);ks(()=>{ws(s[t],r)},n)}function Bn(e,t,n=me,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{if(n.isUnmounted)return;yt();const c=nn(n),l=Me(t,n,e,o);return c(),_t(),l});return s?r.unshift(i):r.push(i),i}}const qe=e=>(t,n=me)=>(!sn||e==="sp")&&Bn(e,(...s)=>t(...s),n),Sl=qe("bm"),Hs=qe("m"),Rl=qe("bu"),Al=qe("u"),Mi=qe("bum"),ks=qe("um"),Pl=qe("sp"),Tl=qe("rtg"),Ol=qe("rtc");function Ml(e,t=me){Bn("ec",e,t)}function Xu(e,t,n,s){let r;const i=n&&n[s];if(q(e)||he(e)){r=new Array(e.length);for(let o=0,c=e.length;ot(o,c,void 0,i&&i[c]));else{const o=Object.keys(e);r=new Array(o.length);for(let c=0,l=o.length;cPn(t)?!(t.type===Pe||t.type===ve&&!Ii(t.children)):!0)?e:null}const hs=e=>e?Ji(e)?Dn(e)||e.proxy:hs(e.parent):null,Vt=pe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>hs(e.parent),$root:e=>hs(e.root),$emit:e=>e.emit,$options:e=>js(e),$forceUpdate:e=>e.f||(e.f=()=>{e.effect.dirty=!0,$n(e.update)}),$nextTick:e=>e.n||(e.n=vi.bind(e.proxy)),$watch:e=>_l.bind(e)}),Yn=(e,t)=>e!==ie&&!e.__isScriptSetup&&ee(e,t),Il={get({_:e},t){const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:c,appContext:l}=e;let h;if(t[0]!=="$"){const y=o[t];if(y!==void 0)switch(y){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(Yn(s,t))return o[t]=1,s[t];if(r!==ie&&ee(r,t))return o[t]=2,r[t];if((h=e.propsOptions[0])&&ee(h,t))return o[t]=3,i[t];if(n!==ie&&ee(n,t))return o[t]=4,n[t];ds&&(o[t]=0)}}const f=Vt[t];let d,p;if(f)return t==="$attrs"&&we(e,"get",t),f(e);if((d=c.__cssModules)&&(d=d[t]))return d;if(n!==ie&&ee(n,t))return o[t]=4,n[t];if(p=l.config.globalProperties,ee(p,t))return p[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return Yn(r,t)?(r[t]=n,!0):s!==ie&&ee(s,t)?(s[t]=n,!0):ee(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:i}},o){let c;return!!n[o]||e!==ie&&ee(e,o)||Yn(t,o)||(c=i[0])&&ee(c,o)||ee(s,o)||ee(Vt,o)||ee(r.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:ee(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function or(e){return q(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let ds=!0;function Ll(e){const t=js(e),n=e.proxy,s=e.ctx;ds=!1,t.beforeCreate&&lr(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:c,provide:l,inject:h,created:f,beforeMount:d,mounted:p,beforeUpdate:y,updated:x,activated:F,deactivated:k,beforeDestroy:I,beforeUnmount:M,destroyed:m,unmounted:E,render:$,renderTracked:P,renderTriggered:G,errorCaptured:H,serverPrefetch:W,expose:j,inheritAttrs:Z,components:A,directives:z,filters:ue}=t;if(h&&Nl(h,s,null),o)for(const Y in o){const V=o[Y];J(V)&&(s[Y]=V.bind(n))}if(r){const Y=r.call(n,n);oe(Y)&&(e.data=Fn(Y))}if(ds=!0,i)for(const Y in i){const V=i[Y],Ie=J(V)?V.bind(n,n):J(V.get)?V.get.bind(n,n):Ae,Ge=!J(V)&&J(V.set)?V.set.bind(n):Ae,je=Fe({get:Ie,set:Ge});Object.defineProperty(s,Y,{enumerable:!0,configurable:!0,get:()=>je.value,set:Ce=>je.value=Ce})}if(c)for(const Y in c)Li(c[Y],s,n,Y);if(l){const Y=J(l)?l.call(n):l;Reflect.ownKeys(Y).forEach(V=>{_n(V,Y[V])})}f&&lr(f,e,"c");function D(Y,V){q(V)?V.forEach(Ie=>Y(Ie.bind(n))):V&&Y(V.bind(n))}if(D(Sl,d),D(Hs,p),D(Rl,y),D(Al,x),D(Cl,F),D(xl,k),D(Ml,H),D(Ol,P),D(Tl,G),D(Mi,M),D(ks,E),D(Pl,W),q(j))if(j.length){const Y=e.exposed||(e.exposed={});j.forEach(V=>{Object.defineProperty(Y,V,{get:()=>n[V],set:Ie=>n[V]=Ie})})}else e.exposed||(e.exposed={});$&&e.render===Ae&&(e.render=$),Z!=null&&(e.inheritAttrs=Z),A&&(e.components=A),z&&(e.directives=z)}function Nl(e,t,n=Ae){q(e)&&(e=ps(e));for(const s in e){const r=e[s];let i;oe(r)?"default"in r?i=He(r.from||s,r.default,!0):i=He(r.from||s):i=He(r),Ee(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function lr(e,t,n){Me(q(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Li(e,t,n,s){const r=s.includes(".")?Ri(n,s):()=>n[s];if(he(e)){const i=t[e];J(i)&&yn(r,i)}else if(J(e))yn(r,e.bind(n));else if(oe(e))if(q(e))e.forEach(i=>Li(i,t,n,s));else{const i=J(e.handler)?e.handler.bind(n):t[e.handler];J(i)&&yn(r,i,e)}}function js(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,c=i.get(t);let l;return c?l=c:!r.length&&!n&&!s?l=t:(l={},r.length&&r.forEach(h=>Rn(l,h,o,!0)),Rn(l,t,o)),oe(t)&&i.set(t,l),l}function Rn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Rn(e,i,n,!0),r&&r.forEach(o=>Rn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const c=Fl[o]||n&&n[o];e[o]=c?c(e[o],t[o]):t[o]}return e}const Fl={data:cr,props:ur,emits:ur,methods:Bt,computed:Bt,beforeCreate:be,created:be,beforeMount:be,mounted:be,beforeUpdate:be,updated:be,beforeDestroy:be,beforeUnmount:be,destroyed:be,unmounted:be,activated:be,deactivated:be,errorCaptured:be,serverPrefetch:be,components:Bt,directives:Bt,watch:Hl,provide:cr,inject:$l};function cr(e,t){return t?e?function(){return pe(J(e)?e.call(this,this):e,J(t)?t.call(this,this):t)}:t:e}function $l(e,t){return Bt(ps(e),ps(t))}function ps(e){if(q(e)){const t={};for(let n=0;n1)return n&&J(t)?t.call(s&&s.proxy):t}}function Bl(e,t,n,s=!1){const r={},i={};En(i,Vn,1),e.propsDefaults=Object.create(null),Fi(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);n?e.props=s?r:ai(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function Vl(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,c=te(r),[l]=e.propsOptions;let h=!1;if((s||o>0)&&!(o&16)){if(o&8){const f=e.vnode.dynamicProps;for(let d=0;d{l=!0;const[p,y]=$i(d,t,!0);pe(o,p),y&&c.push(...y)};!n&&t.mixins.length&&t.mixins.forEach(f),e.extends&&f(e.extends),e.mixins&&e.mixins.forEach(f)}if(!i&&!l)return oe(e)&&s.set(e,xt),xt;if(q(i))for(let f=0;f-1,y[1]=F<0||x-1||ee(y,"default"))&&c.push(d)}}}const h=[o,c];return oe(e)&&s.set(e,h),h}function fr(e){return e[0]!=="$"&&!St(e)}function ar(e){return e===null?"null":typeof e=="function"?e.name||"":typeof e=="object"&&e.constructor&&e.constructor.name||""}function hr(e,t){return ar(e)===ar(t)}function dr(e,t){return q(t)?t.findIndex(n=>hr(n,e)):J(t)&&hr(t,e)?0:-1}const Hi=e=>e[0]==="_"||e==="$stable",Bs=e=>q(e)?e.map(Ne):[Ne(e)],Dl=(e,t,n)=>{if(t._n)return t;const s=ll((...r)=>Bs(t(...r)),n);return s._c=!1,s},ki=(e,t,n)=>{const s=e._ctx;for(const r in e){if(Hi(r))continue;const i=e[r];if(J(i))t[r]=Dl(r,i,s);else if(i!=null){const o=Bs(i);t[r]=()=>o}}},ji=(e,t)=>{const n=Bs(t);e.slots.default=()=>n},Ul=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=te(t),En(t,"_",n)):ki(t,e.slots={})}else e.slots={},t&&ji(e,t);En(e.slots,Vn,1)},Kl=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=ie;if(s.shapeFlag&32){const c=t._;c?n&&c===1?i=!1:(pe(r,t),!n&&c===1&&delete r._):(i=!t.$stable,ki(t,r)),o=t}else t&&(ji(e,t),o={default:1});if(i)for(const c in r)!Hi(c)&&o[c]==null&&delete r[c]};function An(e,t,n,s,r=!1){if(q(e)){e.forEach((p,y)=>An(p,t&&(q(t)?t[y]:t),n,s,r));return}if(Tt(s)&&!r)return;const i=s.shapeFlag&4?Dn(s.component)||s.component.proxy:s.el,o=r?null:i,{i:c,r:l}=e,h=t&&t.r,f=c.refs===ie?c.refs={}:c.refs,d=c.setupState;if(h!=null&&h!==l&&(he(h)?(f[h]=null,ee(d,h)&&(d[h]=null)):Ee(h)&&(h.value=null)),J(l))it(l,c,12,[o,f]);else{const p=he(l),y=Ee(l);if(p||y){const x=()=>{if(e.f){const F=p?ee(d,l)?d[l]:f[l]:l.value;r?q(F)&&ws(F,i):q(F)?F.includes(i)||F.push(i):p?(f[l]=[i],ee(d,l)&&(d[l]=f[l])):(l.value=[i],e.k&&(f[e.k]=l.value))}else p?(f[l]=o,ee(d,l)&&(d[l]=o)):y&&(l.value=o,e.k&&(f[e.k]=o))};o?(x.id=-1,xe(x,n)):x()}}}let Qe=!1;const Wl=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",ql=e=>e.namespaceURI.includes("MathML"),dn=e=>{if(Wl(e))return"svg";if(ql(e))return"mathml"},pn=e=>e.nodeType===8;function Gl(e){const{mt:t,p:n,o:{patchProp:s,createText:r,nextSibling:i,parentNode:o,remove:c,insert:l,createComment:h}}=e,f=(m,E)=>{if(!E.hasChildNodes()){n(null,m,E),wn(),E._vnode=m;return}Qe=!1,d(E.firstChild,m,null,null,null),wn(),E._vnode=m,Qe&&console.error("Hydration completed but contains mismatches.")},d=(m,E,$,P,G,H=!1)=>{const W=pn(m)&&m.data==="[",j=()=>F(m,E,$,P,G,W),{type:Z,ref:A,shapeFlag:z,patchFlag:ue}=E;let fe=m.nodeType;E.el=m,ue===-2&&(H=!1,E.dynamicChildren=null);let D=null;switch(Z){case It:fe!==3?E.children===""?(l(E.el=r(""),o(m),m),D=m):D=j():(m.data!==E.children&&(Qe=!0,m.data=E.children),D=i(m));break;case Pe:M(m)?(D=i(m),I(E.el=m.content.firstChild,m,$)):fe!==8||W?D=j():D=i(m);break;case Ot:if(W&&(m=i(m),fe=m.nodeType),fe===1||fe===3){D=m;const Y=!E.children.length;for(let V=0;V{H=H||!!E.dynamicChildren;const{type:W,props:j,patchFlag:Z,shapeFlag:A,dirs:z,transition:ue}=E,fe=W==="input"||W==="option";if(fe||Z!==-1){z&&Ve(E,null,$,"created");let D=!1;if(M(m)){D=Bi(P,ue)&&$&&$.vnode.props&&$.vnode.props.appear;const V=m.content.firstChild;D&&ue.beforeEnter(V),I(V,m,$),E.el=m=V}if(A&16&&!(j&&(j.innerHTML||j.textContent))){let V=y(m.firstChild,E,m,$,P,G,H);for(;V;){Qe=!0;const Ie=V;V=V.nextSibling,c(Ie)}}else A&8&&m.textContent!==E.children&&(Qe=!0,m.textContent=E.children);if(j)if(fe||!H||Z&48)for(const V in j)(fe&&(V.endsWith("value")||V==="indeterminate")||Zt(V)&&!St(V)||V[0]===".")&&s(m,V,null,j[V],void 0,void 0,$);else j.onClick&&s(m,"onClick",null,j.onClick,void 0,void 0,$);let Y;(Y=j&&j.onVnodeBeforeMount)&&Oe(Y,$,E),z&&Ve(E,null,$,"beforeMount"),((Y=j&&j.onVnodeMounted)||z||D)&&Si(()=>{Y&&Oe(Y,$,E),D&&ue.enter(m),z&&Ve(E,null,$,"mounted")},P)}return m.nextSibling},y=(m,E,$,P,G,H,W)=>{W=W||!!E.dynamicChildren;const j=E.children,Z=j.length;for(let A=0;A{const{slotScopeIds:W}=E;W&&(G=G?G.concat(W):W);const j=o(m),Z=y(i(m),E,j,$,P,G,H);return Z&&pn(Z)&&Z.data==="]"?i(E.anchor=Z):(Qe=!0,l(E.anchor=h("]"),j,Z),Z)},F=(m,E,$,P,G,H)=>{if(Qe=!0,E.el=null,H){const Z=k(m);for(;;){const A=i(m);if(A&&A!==Z)c(A);else break}}const W=i(m),j=o(m);return c(m),n(null,E,j,W,$,P,dn(j),G),W},k=(m,E="[",$="]")=>{let P=0;for(;m;)if(m=i(m),m&&pn(m)&&(m.data===E&&P++,m.data===$)){if(P===0)return i(m);P--}return m},I=(m,E,$)=>{const P=E.parentNode;P&&P.replaceChild(m,E);let G=$;for(;G;)G.vnode.el===E&&(G.vnode.el=G.subTree.el=m),G=G.parent},M=m=>m.nodeType===1&&m.tagName.toLowerCase()==="template";return[f,d]}const xe=Si;function zl(e){return Ql(e,Gl)}function Ql(e,t){const n=Yr();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:c,createComment:l,setText:h,setElementText:f,parentNode:d,nextSibling:p,setScopeId:y=Ae,insertStaticContent:x}=e,F=(u,a,g,v=null,_=null,S=null,O=void 0,w=null,R=!!a.dynamicChildren)=>{if(u===a)return;u&&!ht(u,a)&&(v=b(u),Ce(u,_,S,!0),u=null),a.patchFlag===-2&&(R=!1,a.dynamicChildren=null);const{type:C,ref:N,shapeFlag:K}=a;switch(C){case It:k(u,a,g,v);break;case Pe:I(u,a,g,v);break;case Ot:u==null&&M(a,g,v,O);break;case ve:A(u,a,g,v,_,S,O,w,R);break;default:K&1?$(u,a,g,v,_,S,O,w,R):K&6?z(u,a,g,v,_,S,O,w,R):(K&64||K&128)&&C.process(u,a,g,v,_,S,O,w,R,B)}N!=null&&_&&An(N,u&&u.ref,S,a||u,!a)},k=(u,a,g,v)=>{if(u==null)s(a.el=c(a.children),g,v);else{const _=a.el=u.el;a.children!==u.children&&h(_,a.children)}},I=(u,a,g,v)=>{u==null?s(a.el=l(a.children||""),g,v):a.el=u.el},M=(u,a,g,v)=>{[u.el,u.anchor]=x(u.children,a,g,v,u.el,u.anchor)},m=({el:u,anchor:a},g,v)=>{let _;for(;u&&u!==a;)_=p(u),s(u,g,v),u=_;s(a,g,v)},E=({el:u,anchor:a})=>{let g;for(;u&&u!==a;)g=p(u),r(u),u=g;r(a)},$=(u,a,g,v,_,S,O,w,R)=>{a.type==="svg"?O="svg":a.type==="math"&&(O="mathml"),u==null?P(a,g,v,_,S,O,w,R):W(u,a,_,S,O,w,R)},P=(u,a,g,v,_,S,O,w)=>{let R,C;const{props:N,shapeFlag:K,transition:U,dirs:Q}=u;if(R=u.el=o(u.type,S,N&&N.is,N),K&8?f(R,u.children):K&16&&H(u.children,R,null,v,_,Xn(u,S),O,w),Q&&Ve(u,null,v,"created"),G(R,u,u.scopeId,O,v),N){for(const re in N)re!=="value"&&!St(re)&&i(R,re,null,N[re],S,u.children,v,_,ye);"value"in N&&i(R,"value",null,N.value,S),(C=N.onVnodeBeforeMount)&&Oe(C,v,u)}Q&&Ve(u,null,v,"beforeMount");const X=Bi(_,U);X&&U.beforeEnter(R),s(R,a,g),((C=N&&N.onVnodeMounted)||X||Q)&&xe(()=>{C&&Oe(C,v,u),X&&U.enter(R),Q&&Ve(u,null,v,"mounted")},_)},G=(u,a,g,v,_)=>{if(g&&y(u,g),v)for(let S=0;S{for(let C=R;C{const w=a.el=u.el;let{patchFlag:R,dynamicChildren:C,dirs:N}=a;R|=u.patchFlag&16;const K=u.props||ie,U=a.props||ie;let Q;if(g&&ct(g,!1),(Q=U.onVnodeBeforeUpdate)&&Oe(Q,g,a,u),N&&Ve(a,u,g,"beforeUpdate"),g&&ct(g,!0),C?j(u.dynamicChildren,C,w,g,v,Xn(a,_),S):O||V(u,a,w,null,g,v,Xn(a,_),S,!1),R>0){if(R&16)Z(w,a,K,U,g,v,_);else if(R&2&&K.class!==U.class&&i(w,"class",null,U.class,_),R&4&&i(w,"style",K.style,U.style,_),R&8){const X=a.dynamicProps;for(let re=0;re{Q&&Oe(Q,g,a,u),N&&Ve(a,u,g,"updated")},v)},j=(u,a,g,v,_,S,O)=>{for(let w=0;w{if(g!==v){if(g!==ie)for(const w in g)!St(w)&&!(w in v)&&i(u,w,g[w],null,O,a.children,_,S,ye);for(const w in v){if(St(w))continue;const R=v[w],C=g[w];R!==C&&w!=="value"&&i(u,w,C,R,O,a.children,_,S,ye)}"value"in v&&i(u,"value",g.value,v.value,O)}},A=(u,a,g,v,_,S,O,w,R)=>{const C=a.el=u?u.el:c(""),N=a.anchor=u?u.anchor:c("");let{patchFlag:K,dynamicChildren:U,slotScopeIds:Q}=a;Q&&(w=w?w.concat(Q):Q),u==null?(s(C,g,v),s(N,g,v),H(a.children||[],g,N,_,S,O,w,R)):K>0&&K&64&&U&&u.dynamicChildren?(j(u.dynamicChildren,U,g,_,S,O,w),(a.key!=null||_&&a===_.subTree)&&Vi(u,a,!0)):V(u,a,g,N,_,S,O,w,R)},z=(u,a,g,v,_,S,O,w,R)=>{a.slotScopeIds=w,u==null?a.shapeFlag&512?_.ctx.activate(a,g,v,O,R):ue(a,g,v,_,S,O,R):fe(u,a,R)},ue=(u,a,g,v,_,S,O)=>{const w=u.component=rc(u,v,_);if(tn(u)&&(w.ctx.renderer=B),ic(w),w.asyncDep){if(_&&_.registerDep(w,D),!u.el){const R=w.subTree=ae(Pe);I(null,R,a,g)}}else D(w,u,a,g,_,S,O)},fe=(u,a,g)=>{const v=a.component=u.component;if(fl(u,a,g))if(v.asyncDep&&!v.asyncResolved){Y(v,a,g);return}else v.next=a,sl(v.update),v.effect.dirty=!0,v.update();else a.el=u.el,v.vnode=a},D=(u,a,g,v,_,S,O)=>{const w=()=>{if(u.isMounted){let{next:N,bu:K,u:U,parent:Q,vnode:X}=u;{const Et=Di(u);if(Et){N&&(N.el=X.el,Y(u,N,O)),Et.asyncDep.then(()=>{u.isUnmounted||w()});return}}let re=N,ce;ct(u,!1),N?(N.el=X.el,Y(u,N,O)):N=X,K&&qn(K),(ce=N.props&&N.props.onVnodeBeforeUpdate)&&Oe(ce,Q,N,X),ct(u,!0);const ge=zn(u),Le=u.subTree;u.subTree=ge,F(Le,ge,d(Le.el),b(Le),u,_,S),N.el=ge.el,re===null&&al(u,ge.el),U&&xe(U,_),(ce=N.props&&N.props.onVnodeUpdated)&&xe(()=>Oe(ce,Q,N,X),_)}else{let N;const{el:K,props:U}=a,{bm:Q,m:X,parent:re}=u,ce=Tt(a);if(ct(u,!1),Q&&qn(Q),!ce&&(N=U&&U.onVnodeBeforeMount)&&Oe(N,re,a),ct(u,!0),K&&le){const ge=()=>{u.subTree=zn(u),le(K,u.subTree,u,_,null)};ce?a.type.__asyncLoader().then(()=>!u.isUnmounted&&ge()):ge()}else{const ge=u.subTree=zn(u);F(null,ge,g,v,u,_,S),a.el=ge.el}if(X&&xe(X,_),!ce&&(N=U&&U.onVnodeMounted)){const ge=a;xe(()=>Oe(N,re,ge),_)}(a.shapeFlag&256||re&&Tt(re.vnode)&&re.vnode.shapeFlag&256)&&u.a&&xe(u.a,_),u.isMounted=!0,a=g=v=null}},R=u.effect=new Ps(w,Ae,()=>$n(C),u.scope),C=u.update=()=>{R.dirty&&R.run()};C.id=u.uid,ct(u,!0),C()},Y=(u,a,g)=>{a.component=u;const v=u.vnode.props;u.vnode=a,u.next=null,Vl(u,a.props,v,g),Kl(u,a.children,g),yt(),nr(u),_t()},V=(u,a,g,v,_,S,O,w,R=!1)=>{const C=u&&u.children,N=u?u.shapeFlag:0,K=a.children,{patchFlag:U,shapeFlag:Q}=a;if(U>0){if(U&128){Ge(C,K,g,v,_,S,O,w,R);return}else if(U&256){Ie(C,K,g,v,_,S,O,w,R);return}}Q&8?(N&16&&ye(C,_,S),K!==C&&f(g,K)):N&16?Q&16?Ge(C,K,g,v,_,S,O,w,R):ye(C,_,S,!0):(N&8&&f(g,""),Q&16&&H(K,g,v,_,S,O,w,R))},Ie=(u,a,g,v,_,S,O,w,R)=>{u=u||xt,a=a||xt;const C=u.length,N=a.length,K=Math.min(C,N);let U;for(U=0;UN?ye(u,_,S,!0,!1,K):H(a,g,v,_,S,O,w,R,K)},Ge=(u,a,g,v,_,S,O,w,R)=>{let C=0;const N=a.length;let K=u.length-1,U=N-1;for(;C<=K&&C<=U;){const Q=u[C],X=a[C]=R?tt(a[C]):Ne(a[C]);if(ht(Q,X))F(Q,X,g,null,_,S,O,w,R);else break;C++}for(;C<=K&&C<=U;){const Q=u[K],X=a[U]=R?tt(a[U]):Ne(a[U]);if(ht(Q,X))F(Q,X,g,null,_,S,O,w,R);else break;K--,U--}if(C>K){if(C<=U){const Q=U+1,X=QU)for(;C<=K;)Ce(u[C],_,S,!0),C++;else{const Q=C,X=C,re=new Map;for(C=X;C<=U;C++){const Se=a[C]=R?tt(a[C]):Ne(a[C]);Se.key!=null&&re.set(Se.key,C)}let ce,ge=0;const Le=U-X+1;let Et=!1,Ws=0;const $t=new Array(Le);for(C=0;C=Le){Ce(Se,_,S,!0);continue}let Be;if(Se.key!=null)Be=re.get(Se.key);else for(ce=X;ce<=U;ce++)if($t[ce-X]===0&&ht(Se,a[ce])){Be=ce;break}Be===void 0?Ce(Se,_,S,!0):($t[Be-X]=C+1,Be>=Ws?Ws=Be:Et=!0,F(Se,a[Be],g,null,_,S,O,w,R),ge++)}const qs=Et?Jl($t):xt;for(ce=qs.length-1,C=Le-1;C>=0;C--){const Se=X+C,Be=a[Se],Gs=Se+1{const{el:S,type:O,transition:w,children:R,shapeFlag:C}=u;if(C&6){je(u.component.subTree,a,g,v);return}if(C&128){u.suspense.move(a,g,v);return}if(C&64){O.move(u,a,g,B);return}if(O===ve){s(S,a,g);for(let K=0;Kw.enter(S),_);else{const{leave:K,delayLeave:U,afterLeave:Q}=w,X=()=>s(S,a,g),re=()=>{K(S,()=>{X(),Q&&Q()})};U?U(S,X,re):re()}else s(S,a,g)},Ce=(u,a,g,v=!1,_=!1)=>{const{type:S,props:O,ref:w,children:R,dynamicChildren:C,shapeFlag:N,patchFlag:K,dirs:U}=u;if(w!=null&&An(w,null,g,u,!0),N&256){a.ctx.deactivate(u);return}const Q=N&1&&U,X=!Tt(u);let re;if(X&&(re=O&&O.onVnodeBeforeUnmount)&&Oe(re,a,u),N&6)rn(u.component,g,v);else{if(N&128){u.suspense.unmount(g,v);return}Q&&Ve(u,null,a,"beforeUnmount"),N&64?u.type.remove(u,a,g,_,B,v):C&&(S!==ve||K>0&&K&64)?ye(C,a,g,!1,!0):(S===ve&&K&384||!_&&N&16)&&ye(R,a,g),v&&bt(u)}(X&&(re=O&&O.onVnodeUnmounted)||Q)&&xe(()=>{re&&Oe(re,a,u),Q&&Ve(u,null,a,"unmounted")},g)},bt=u=>{const{type:a,el:g,anchor:v,transition:_}=u;if(a===ve){vt(g,v);return}if(a===Ot){E(u);return}const S=()=>{r(g),_&&!_.persisted&&_.afterLeave&&_.afterLeave()};if(u.shapeFlag&1&&_&&!_.persisted){const{leave:O,delayLeave:w}=_,R=()=>O(g,S);w?w(u.el,S,R):R()}else S()},vt=(u,a)=>{let g;for(;u!==a;)g=p(u),r(u),u=g;r(a)},rn=(u,a,g)=>{const{bum:v,scope:_,update:S,subTree:O,um:w}=u;v&&qn(v),_.stop(),S&&(S.active=!1,Ce(O,u,a,g)),w&&xe(w,a),xe(()=>{u.isUnmounted=!0},a),a&&a.pendingBranch&&!a.isUnmounted&&u.asyncDep&&!u.asyncResolved&&u.suspenseId===a.pendingId&&(a.deps--,a.deps===0&&a.resolve())},ye=(u,a,g,v=!1,_=!1,S=0)=>{for(let O=S;Ou.shapeFlag&6?b(u.component.subTree):u.shapeFlag&128?u.suspense.next():p(u.anchor||u.el);let L=!1;const T=(u,a,g)=>{u==null?a._vnode&&Ce(a._vnode,null,null,!0):F(a._vnode||null,u,a,null,null,null,g),L||(L=!0,nr(),wn(),L=!1),a._vnode=u},B={p:F,um:Ce,m:je,r:bt,mt:ue,mc:H,pc:V,pbc:j,n:b,o:e};let ne,le;return t&&([ne,le]=t(B)),{render:T,hydrate:ne,createApp:jl(T,ne)}}function Xn({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function ct({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function Bi(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Vi(e,t,n=!1){const s=e.children,r=t.children;if(q(s)&&q(r))for(let i=0;i>1,e[n[c]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function Di(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Di(t)}const Yl=e=>e.__isTeleport,ve=Symbol.for("v-fgt"),It=Symbol.for("v-txt"),Pe=Symbol.for("v-cmt"),Ot=Symbol.for("v-stc"),Ut=[];let $e=null;function Ui(e=!1){Ut.push($e=e?null:[])}function Xl(){Ut.pop(),$e=Ut[Ut.length-1]||null}let Qt=1;function pr(e){Qt+=e}function Ki(e){return e.dynamicChildren=Qt>0?$e||xt:null,Xl(),Qt>0&&$e&&$e.push(e),e}function ef(e,t,n,s,r,i){return Ki(Gi(e,t,n,s,r,i,!0))}function Wi(e,t,n,s,r){return Ki(ae(e,t,n,s,r,!0))}function Pn(e){return e?e.__v_isVNode===!0:!1}function ht(e,t){return e.type===t.type&&e.key===t.key}const Vn="__vInternal",qi=({key:e})=>e??null,bn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?he(e)||Ee(e)||J(e)?{i:de,r:e,k:t,f:!!n}:e:null);function Gi(e,t=null,n=null,s=0,r=null,i=e===ve?0:1,o=!1,c=!1){const l={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&qi(t),ref:t&&bn(t),scopeId:kn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:de};return c?(Vs(l,n),i&128&&e.normalize(l)):n&&(l.shapeFlag|=he(n)?8:16),Qt>0&&!o&&$e&&(l.patchFlag>0||i&6)&&l.patchFlag!==32&&$e.push(l),l}const ae=Zl;function Zl(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===hl)&&(e=Pe),Pn(e)){const c=lt(e,t,!0);return n&&Vs(c,n),Qt>0&&!i&&$e&&(c.shapeFlag&6?$e[$e.indexOf(e)]=c:$e.push(c)),c.patchFlag|=-2,c}if(fc(e)&&(e=e.__vccOpts),t){t=ec(t);let{class:c,style:l}=t;c&&!he(c)&&(t.class=As(c)),oe(l)&&(di(l)&&!q(l)&&(l=pe({},l)),t.style=Rs(l))}const o=he(e)?1:pl(e)?128:Yl(e)?64:oe(e)?4:J(e)?2:0;return Gi(e,t,n,s,r,o,i,!0)}function ec(e){return e?di(e)||Vn in e?pe({},e):e:null}function lt(e,t,n=!1){const{props:s,ref:r,patchFlag:i,children:o}=e,c=t?tc(s||{},t):s;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:c,key:c&&qi(c),ref:t&&t.ref?n&&r?q(r)?r.concat(bn(t)):[r,bn(t)]:bn(t):r,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ve?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&<(e.ssContent),ssFallback:e.ssFallback&<(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function zi(e=" ",t=0){return ae(It,null,e,t)}function tf(e,t){const n=ae(Ot,null,e);return n.staticCount=t,n}function nf(e="",t=!1){return t?(Ui(),Wi(Pe,null,e)):ae(Pe,null,e)}function Ne(e){return e==null||typeof e=="boolean"?ae(Pe):q(e)?ae(ve,null,e.slice()):typeof e=="object"?tt(e):ae(It,null,String(e))}function tt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:lt(e)}function Vs(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(q(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),Vs(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!(Vn in t)?t._ctx=de:r===3&&de&&(de.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else J(t)?(t={default:t,_ctx:de},n=32):(t=String(t),s&64?(n=16,t=[zi(t)]):n=8);e.children=t,e.shapeFlag|=n}function tc(...e){const t={};for(let n=0;nme||de;let Tn,ms;{const e=Yr(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};Tn=t("__VUE_INSTANCE_SETTERS__",n=>me=n),ms=t("__VUE_SSR_SETTERS__",n=>sn=n)}const nn=e=>{const t=me;return Tn(e),e.scope.on(),()=>{e.scope.off(),Tn(t)}},gr=()=>{me&&me.scope.off(),Tn(null)};function Ji(e){return e.vnode.shapeFlag&4}let sn=!1;function ic(e,t=!1){t&&ms(t);const{props:n,children:s}=e.vnode,r=Ji(e);Bl(e,n,r,t),Ul(e,s);const i=r?oc(e,t):void 0;return t&&ms(!1),i}function oc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=pi(new Proxy(e.ctx,Il));const{setup:s}=n;if(s){const r=e.setupContext=s.length>1?cc(e):null,i=nn(e);yt();const o=it(s,e,0,[e.props,r]);if(_t(),i(),zr(o)){if(o.then(gr,gr),t)return o.then(c=>{mr(e,c,t)}).catch(c=>{en(c,e,0)});e.asyncDep=o}else mr(e,o,t)}else Yi(e,t)}function mr(e,t,n){J(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:oe(t)&&(e.setupState=_i(t)),Yi(e,n)}let yr;function Yi(e,t,n){const s=e.type;if(!e.render){if(!t&&yr&&!s.render){const r=s.template||js(e).template;if(r){const{isCustomElement:i,compilerOptions:o}=e.appContext.config,{delimiters:c,compilerOptions:l}=s,h=pe(pe({isCustomElement:i,delimiters:c},o),l);s.render=yr(r,h)}}e.render=s.render||Ae}{const r=nn(e);yt();try{Ll(e)}finally{_t(),r()}}}function lc(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get(t,n){return we(e,"get","$attrs"),t[n]}}))}function cc(e){const t=n=>{e.exposed=n||{}};return{get attrs(){return lc(e)},slots:e.slots,emit:e.emit,expose:t}}function Dn(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(_i(pi(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Vt)return Vt[n](e)},has(t,n){return n in t||n in Vt}}))}function uc(e,t=!0){return J(e)?e.displayName||e.name:e.name||t&&e.__name}function fc(e){return J(e)&&"__vccOpts"in e}const Fe=(e,t)=>Qo(e,t,sn);function Ds(e,t,n){const s=arguments.length;return s===2?oe(t)&&!q(t)?Pn(t)?ae(e,null,[t]):ae(e,t):ae(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&Pn(n)&&(n=[n]),ae(e,t,n))}const ac="3.4.21";/**
+* @vue/runtime-dom v3.4.21
+* (c) 2018-present Yuxi (Evan) You and Vue contributors
+* @license MIT
+**/const hc="http://www.w3.org/2000/svg",dc="http://www.w3.org/1998/Math/MathML",nt=typeof document<"u"?document:null,_r=nt&&nt.createElement("template"),pc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?nt.createElementNS(hc,e):t==="mathml"?nt.createElementNS(dc,e):nt.createElement(e,n?{is:n}:void 0);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>nt.createTextNode(e),createComment:e=>nt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>nt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{_r.innerHTML=s==="svg"?``:s==="mathml"?``:e;const c=_r.content;if(s==="svg"||s==="mathml"){const l=c.firstChild;for(;l.firstChild;)c.appendChild(l.firstChild);c.removeChild(l)}t.insertBefore(c,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Je="transition",Ht="animation",Jt=Symbol("_vtc"),Xi=(e,{slots:t})=>Ds(El,gc(e),t);Xi.displayName="Transition";const Zi={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};Xi.props=pe({},Ai,Zi);const ut=(e,t=[])=>{q(e)?e.forEach(n=>n(...t)):e&&e(...t)},br=e=>e?q(e)?e.some(t=>t.length>1):e.length>1:!1;function gc(e){const t={};for(const A in e)A in Zi||(t[A]=e[A]);if(e.css===!1)return t;const{name:n="v",type:s,duration:r,enterFromClass:i=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:c=`${n}-enter-to`,appearFromClass:l=i,appearActiveClass:h=o,appearToClass:f=c,leaveFromClass:d=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:y=`${n}-leave-to`}=e,x=mc(r),F=x&&x[0],k=x&&x[1],{onBeforeEnter:I,onEnter:M,onEnterCancelled:m,onLeave:E,onLeaveCancelled:$,onBeforeAppear:P=I,onAppear:G=M,onAppearCancelled:H=m}=t,W=(A,z,ue)=>{ft(A,z?f:c),ft(A,z?h:o),ue&&ue()},j=(A,z)=>{A._isLeaving=!1,ft(A,d),ft(A,y),ft(A,p),z&&z()},Z=A=>(z,ue)=>{const fe=A?G:M,D=()=>W(z,A,ue);ut(fe,[z,D]),vr(()=>{ft(z,A?l:i),Ye(z,A?f:c),br(fe)||Er(z,s,F,D)})};return pe(t,{onBeforeEnter(A){ut(I,[A]),Ye(A,i),Ye(A,o)},onBeforeAppear(A){ut(P,[A]),Ye(A,l),Ye(A,h)},onEnter:Z(!1),onAppear:Z(!0),onLeave(A,z){A._isLeaving=!0;const ue=()=>j(A,z);Ye(A,d),bc(),Ye(A,p),vr(()=>{A._isLeaving&&(ft(A,d),Ye(A,y),br(E)||Er(A,s,k,ue))}),ut(E,[A,ue])},onEnterCancelled(A){W(A,!1),ut(m,[A])},onAppearCancelled(A){W(A,!0),ut(H,[A])},onLeaveCancelled(A){j(A),ut($,[A])}})}function mc(e){if(e==null)return null;if(oe(e))return[Zn(e.enter),Zn(e.leave)];{const t=Zn(e);return[t,t]}}function Zn(e){return bo(e)}function Ye(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[Jt]||(e[Jt]=new Set)).add(t)}function ft(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[Jt];n&&(n.delete(t),n.size||(e[Jt]=void 0))}function vr(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let yc=0;function Er(e,t,n,s){const r=e._endId=++yc,i=()=>{r===e._endId&&s()};if(n)return setTimeout(i,n);const{type:o,timeout:c,propCount:l}=_c(e,t);if(!o)return s();const h=o+"end";let f=0;const d=()=>{e.removeEventListener(h,p),i()},p=y=>{y.target===e&&++f>=l&&d()};setTimeout(()=>{f(n[x]||"").split(", "),r=s(`${Je}Delay`),i=s(`${Je}Duration`),o=Cr(r,i),c=s(`${Ht}Delay`),l=s(`${Ht}Duration`),h=Cr(c,l);let f=null,d=0,p=0;t===Je?o>0&&(f=Je,d=o,p=i.length):t===Ht?h>0&&(f=Ht,d=h,p=l.length):(d=Math.max(o,h),f=d>0?o>h?Je:Ht:null,p=f?f===Je?i.length:l.length:0);const y=f===Je&&/\b(transform|all)(,|$)/.test(s(`${Je}Property`).toString());return{type:f,timeout:d,propCount:p,hasTransform:y}}function Cr(e,t){for(;e.lengthxr(n)+xr(e[s])))}function xr(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function bc(){return document.body.offsetHeight}function vc(e,t,n){const s=e[Jt];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const On=Symbol("_vod"),eo=Symbol("_vsh"),sf={beforeMount(e,{value:t},{transition:n}){e[On]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):kt(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:s}){!t!=!n&&(s?t?(s.beforeEnter(e),kt(e,!0),s.enter(e)):s.leave(e,()=>{kt(e,!1)}):kt(e,t))},beforeUnmount(e,{value:t}){kt(e,t)}};function kt(e,t){e.style.display=t?e[On]:"none",e[eo]=!t}const to=Symbol("");function rf(e){const t=Qi();if(!t)return;const n=t.ut=(r=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach(i=>_s(i,r))},s=()=>{const r=e(t.proxy);ys(t.subTree,r),n(r)};yl(s),Hs(()=>{const r=new MutationObserver(s);r.observe(t.subTree.el.parentNode,{childList:!0}),ks(()=>r.disconnect())})}function ys(e,t){if(e.shapeFlag&128){const n=e.suspense;e=n.activeBranch,n.pendingBranch&&!n.isHydrating&&n.effects.push(()=>{ys(n.activeBranch,t)})}for(;e.component;)e=e.component.subTree;if(e.shapeFlag&1&&e.el)_s(e.el,t);else if(e.type===ve)e.children.forEach(n=>ys(n,t));else if(e.type===Ot){let{el:n,anchor:s}=e;for(;n&&(_s(n,t),n!==s);)n=n.nextSibling}}function _s(e,t){if(e.nodeType===1){const n=e.style;let s="";for(const r in t)n.setProperty(`--${r}`,t[r]),s+=`--${r}: ${t[r]};`;n[to]=s}}const Ec=/(^|;)\s*display\s*:/;function Cc(e,t,n){const s=e.style,r=he(n);let i=!1;if(n&&!r){if(t)if(he(t))for(const o of t.split(";")){const c=o.slice(0,o.indexOf(":")).trim();n[c]==null&&vn(s,c,"")}else for(const o in t)n[o]==null&&vn(s,o,"");for(const o in n)o==="display"&&(i=!0),vn(s,o,n[o])}else if(r){if(t!==n){const o=s[to];o&&(n+=";"+o),s.cssText=n,i=Ec.test(n)}}else t&&e.removeAttribute("style");On in e&&(e[On]=i?s.display:"",e[eo]&&(s.display="none"))}const wr=/\s*!important$/;function vn(e,t,n){if(q(n))n.forEach(s=>vn(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=xc(e,t);wr.test(n)?e.setProperty(mt(s),n.replace(wr,""),"important"):e[s]=n}}const Sr=["Webkit","Moz","ms"],es={};function xc(e,t){const n=es[t];if(n)return n;let s=Ue(t);if(s!=="filter"&&s in e)return es[t]=s;s=Ln(s);for(let r=0;rts||(Oc.then(()=>ts=0),ts=Date.now());function Ic(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;Me(Lc(s,n.value),t,5,[s])};return n.value=e,n.attached=Mc(),n}function Lc(e,t){if(q(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const Tr=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Nc=(e,t,n,s,r,i,o,c,l)=>{const h=r==="svg";t==="class"?vc(e,s,h):t==="style"?Cc(e,n,s):Zt(t)?xs(t)||Pc(e,t,n,s,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Fc(e,t,s,h))?Sc(e,t,s,i,o,c,l):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),wc(e,t,s,h))};function Fc(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Tr(t)&&J(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Tr(t)&&he(n)?!1:t in e}const $c={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},of=(e,t)=>{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=r=>{if(!("key"in r))return;const i=mt(r.key);if(t.some(o=>o===i||$c[o]===i))return e(r)})},Hc=pe({patchProp:Nc},pc);let ns,Or=!1;function kc(){return ns=Or?ns:zl(Hc),Or=!0,ns}const lf=(...e)=>{const t=kc().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Bc(s);if(r)return n(r,!0,jc(r))},t};function jc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Bc(e){return he(e)?document.querySelector(e):e}var Vc=([e,t,n])=>e==="meta"&&t.name?`${e}.${t.name}`:["title","base"].includes(e)?e:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,t,n]),cf=e=>{const t=new Set,n=[];return e.forEach(s=>{const r=Vc(s);t.has(r)||(t.add(r),n.push(s))}),n},uf=e=>/^(https?:)?\/\//.test(e),ff=e=>/^mailto:/.test(e),af=e=>/^tel:/.test(e),hf=e=>Object.prototype.toString.call(e)==="[object Object]",df=e=>e.replace(/\/$/,""),pf=e=>e.replace(/^\//,""),gf=(e,t)=>{const n=Object.keys(e).sort((s,r)=>{const i=r.split("/").length-s.split("/").length;return i!==0?i:r.length-s.length});for(const s of n)if(t.startsWith(s))return s;return"/"},mf=(e,t="/")=>e.replace(/^(https?:)?\/\/[^/]*/,"").replace(new RegExp(`^${t}`),"/");/*!
+ * vue-router v4.3.0
+ * (c) 2024 Eduardo San Martin Morote
+ * @license MIT
+ */const Ct=typeof document<"u";function Dc(e){return e.__esModule||e[Symbol.toStringTag]==="Module"}const se=Object.assign;function ss(e,t){const n={};for(const s in t){const r=t[s];n[s]=ke(r)?r.map(e):e(r)}return n}const Kt=()=>{},ke=Array.isArray,no=/#/g,Uc=/&/g,Kc=/\//g,Wc=/=/g,qc=/\?/g,so=/\+/g,Gc=/%5B/g,zc=/%5D/g,ro=/%5E/g,Qc=/%60/g,io=/%7B/g,Jc=/%7C/g,oo=/%7D/g,Yc=/%20/g;function Us(e){return encodeURI(""+e).replace(Jc,"|").replace(Gc,"[").replace(zc,"]")}function Xc(e){return Us(e).replace(io,"{").replace(oo,"}").replace(ro,"^")}function bs(e){return Us(e).replace(so,"%2B").replace(Yc,"+").replace(no,"%23").replace(Uc,"%26").replace(Qc,"`").replace(io,"{").replace(oo,"}").replace(ro,"^")}function Zc(e){return bs(e).replace(Wc,"%3D")}function eu(e){return Us(e).replace(no,"%23").replace(qc,"%3F")}function tu(e){return e==null?"":eu(e).replace(Kc,"%2F")}function Yt(e){try{return decodeURIComponent(""+e)}catch{}return""+e}const nu=/\/$/,su=e=>e.replace(nu,"");function rs(e,t,n="/"){let s,r={},i="",o="";const c=t.indexOf("#");let l=t.indexOf("?");return c=0&&(l=-1),l>-1&&(s=t.slice(0,l),i=t.slice(l+1,c>-1?c:t.length),r=e(i)),c>-1&&(s=s||t.slice(0,c),o=t.slice(c,t.length)),s=lu(s??t,n),{fullPath:s+(i&&"?")+i+o,path:s,query:r,hash:Yt(o)}}function ru(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Mr(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function iu(e,t,n){const s=t.matched.length-1,r=n.matched.length-1;return s>-1&&s===r&&Lt(t.matched[s],n.matched[r])&&lo(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Lt(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function lo(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!ou(e[n],t[n]))return!1;return!0}function ou(e,t){return ke(e)?Ir(e,t):ke(t)?Ir(t,e):e===t}function Ir(e,t){return ke(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function lu(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/"),r=s[s.length-1];(r===".."||r===".")&&s.push("");let i=n.length-1,o,c;for(o=0;o1&&i--;else break;return n.slice(0,i).join("/")+"/"+s.slice(o).join("/")}var Xt;(function(e){e.pop="pop",e.push="push"})(Xt||(Xt={}));var Wt;(function(e){e.back="back",e.forward="forward",e.unknown=""})(Wt||(Wt={}));function cu(e){if(!e)if(Ct){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),su(e)}const uu=/^[^#]+#/;function fu(e,t){return e.replace(uu,"#")+t}function au(e,t){const n=document.documentElement.getBoundingClientRect(),s=e.getBoundingClientRect();return{behavior:t.behavior,left:s.left-n.left-(t.left||0),top:s.top-n.top-(t.top||0)}}const Un=()=>({left:window.scrollX,top:window.scrollY});function hu(e){let t;if("el"in e){const n=e.el,s=typeof n=="string"&&n.startsWith("#"),r=typeof n=="string"?s?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!r)return;t=au(r,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function Lr(e,t){return(history.state?history.state.position-t:-1)+e}const vs=new Map;function du(e,t){vs.set(e,t)}function pu(e){const t=vs.get(e);return vs.delete(e),t}let gu=()=>location.protocol+"//"+location.host;function co(e,t){const{pathname:n,search:s,hash:r}=t,i=e.indexOf("#");if(i>-1){let c=r.includes(e.slice(i))?e.slice(i).length:1,l=r.slice(c);return l[0]!=="/"&&(l="/"+l),Mr(l,"")}return Mr(n,e)+s+r}function mu(e,t,n,s){let r=[],i=[],o=null;const c=({state:p})=>{const y=co(e,location),x=n.value,F=t.value;let k=0;if(p){if(n.value=y,t.value=p,o&&o===x){o=null;return}k=F?p.position-F.position:0}else s(y);r.forEach(I=>{I(n.value,x,{delta:k,type:Xt.pop,direction:k?k>0?Wt.forward:Wt.back:Wt.unknown})})};function l(){o=n.value}function h(p){r.push(p);const y=()=>{const x=r.indexOf(p);x>-1&&r.splice(x,1)};return i.push(y),y}function f(){const{history:p}=window;p.state&&p.replaceState(se({},p.state,{scroll:Un()}),"")}function d(){for(const p of i)p();i=[],window.removeEventListener("popstate",c),window.removeEventListener("beforeunload",f)}return window.addEventListener("popstate",c),window.addEventListener("beforeunload",f,{passive:!0}),{pauseListeners:l,listen:h,destroy:d}}function Nr(e,t,n,s=!1,r=!1){return{back:e,current:t,forward:n,replaced:s,position:window.history.length,scroll:r?Un():null}}function yu(e){const{history:t,location:n}=window,s={value:co(e,n)},r={value:t.state};r.value||i(s.value,{back:null,current:s.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(l,h,f){const d=e.indexOf("#"),p=d>-1?(n.host&&document.querySelector("base")?e:e.slice(d))+l:gu()+e+l;try{t[f?"replaceState":"pushState"](h,"",p),r.value=h}catch(y){console.error(y),n[f?"replace":"assign"](p)}}function o(l,h){const f=se({},t.state,Nr(r.value.back,l,r.value.forward,!0),h,{position:r.value.position});i(l,f,!0),s.value=l}function c(l,h){const f=se({},r.value,t.state,{forward:l,scroll:Un()});i(f.current,f,!0);const d=se({},Nr(s.value,l,null),{position:f.position+1},h);i(l,d,!1),s.value=l}return{location:s,state:r,push:c,replace:o}}function yf(e){e=cu(e);const t=yu(e),n=mu(e,t.state,t.location,t.replace);function s(i,o=!0){o||n.pauseListeners(),history.go(i)}const r=se({location:"",base:e,go:s,createHref:fu.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}function _u(e){return typeof e=="string"||e&&typeof e=="object"}function uo(e){return typeof e=="string"||typeof e=="symbol"}const Xe={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},fo=Symbol("");var Fr;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(Fr||(Fr={}));function Nt(e,t){return se(new Error,{type:e,[fo]:!0},t)}function Ke(e,t){return e instanceof Error&&fo in e&&(t==null||!!(e.type&t))}const $r="[^/]+?",bu={sensitive:!1,strict:!1,start:!0,end:!0},vu=/[.+*?^${}()[\]/\\]/g;function Eu(e,t){const n=se({},bu,t),s=[];let r=n.start?"^":"";const i=[];for(const h of e){const f=h.length?[]:[90];n.strict&&!h.length&&(r+="/");for(let d=0;dt.length?t.length===1&&t[0]===40+40?1:-1:0}function xu(e,t){let n=0;const s=e.score,r=t.score;for(;n0&&t[t.length-1]<0}const wu={type:0,value:""},Su=/[a-zA-Z0-9_]/;function Ru(e){if(!e)return[[]];if(e==="/")return[[wu]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(y){throw new Error(`ERR (${n})/"${h}": ${y}`)}let n=0,s=n;const r=[];let i;function o(){i&&r.push(i),i=[]}let c=0,l,h="",f="";function d(){h&&(n===0?i.push({type:0,value:h}):n===1||n===2||n===3?(i.length>1&&(l==="*"||l==="+")&&t(`A repeatable param (${h}) must be alone in its segment. eg: '/:ids+.`),i.push({type:1,value:h,regexp:f,repeatable:l==="*"||l==="+",optional:l==="*"||l==="?"})):t("Invalid state to consume buffer"),h="")}function p(){h+=l}for(;c{o(M)}:Kt}function o(f){if(uo(f)){const d=s.get(f);d&&(s.delete(f),n.splice(n.indexOf(d),1),d.children.forEach(o),d.alias.forEach(o))}else{const d=n.indexOf(f);d>-1&&(n.splice(d,1),f.record.name&&s.delete(f.record.name),f.children.forEach(o),f.alias.forEach(o))}}function c(){return n}function l(f){let d=0;for(;d=0&&(f.record.path!==n[d].record.path||!ao(f,n[d]));)d++;n.splice(d,0,f),f.record.name&&!jr(f)&&s.set(f.record.name,f)}function h(f,d){let p,y={},x,F;if("name"in f&&f.name){if(p=s.get(f.name),!p)throw Nt(1,{location:f});F=p.record.name,y=se(kr(d.params,p.keys.filter(M=>!M.optional).concat(p.parent?p.parent.keys.filter(M=>M.optional):[]).map(M=>M.name)),f.params&&kr(f.params,p.keys.map(M=>M.name))),x=p.stringify(y)}else if(f.path!=null)x=f.path,p=n.find(M=>M.re.test(x)),p&&(y=p.parse(x),F=p.record.name);else{if(p=d.name?s.get(d.name):n.find(M=>M.re.test(d.path)),!p)throw Nt(1,{location:f,currentLocation:d});F=p.record.name,y=se({},d.params,f.params),x=p.stringify(y)}const k=[];let I=p;for(;I;)k.unshift(I.record),I=I.parent;return{name:F,path:x,params:y,matched:k,meta:Mu(k)}}return e.forEach(f=>i(f)),{addRoute:i,resolve:h,removeRoute:o,getRoutes:c,getRecordMatcher:r}}function kr(e,t){const n={};for(const s of t)s in e&&(n[s]=e[s]);return n}function Tu(e){return{path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:void 0,beforeEnter:e.beforeEnter,props:Ou(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}}}function Ou(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const s in e.components)t[s]=typeof n=="object"?n[s]:n;return t}function jr(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Mu(e){return e.reduce((t,n)=>se(t,n.meta),{})}function Br(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}function ao(e,t){return t.children.some(n=>n===e||ao(e,n))}function Iu(e){const t={};if(e===""||e==="?")return t;const s=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;ri&&bs(i)):[s&&bs(s)]).forEach(i=>{i!==void 0&&(t+=(t.length?"&":"")+n,i!=null&&(t+="="+i))})}return t}function Lu(e){const t={};for(const n in e){const s=e[n];s!==void 0&&(t[n]=ke(s)?s.map(r=>r==null?null:""+r):s==null?s:""+s)}return t}const Nu=Symbol(""),Dr=Symbol(""),Kn=Symbol(""),Ks=Symbol(""),Es=Symbol("");function jt(){let e=[];function t(s){return e.push(s),()=>{const r=e.indexOf(s);r>-1&&e.splice(r,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function st(e,t,n,s,r,i=o=>o()){const o=s&&(s.enterCallbacks[r]=s.enterCallbacks[r]||[]);return()=>new Promise((c,l)=>{const h=p=>{p===!1?l(Nt(4,{from:n,to:t})):p instanceof Error?l(p):_u(p)?l(Nt(2,{from:t,to:p})):(o&&s.enterCallbacks[r]===o&&typeof p=="function"&&o.push(p),c())},f=i(()=>e.call(s&&s.instances[r],t,n,h));let d=Promise.resolve(f);e.length<3&&(d=d.then(h)),d.catch(p=>l(p))})}function is(e,t,n,s,r=i=>i()){const i=[];for(const o of e)for(const c in o.components){let l=o.components[c];if(!(t!=="beforeRouteEnter"&&!o.instances[c]))if(Fu(l)){const f=(l.__vccOpts||l)[t];f&&i.push(st(f,n,s,o,c,r))}else{let h=l();i.push(()=>h.then(f=>{if(!f)return Promise.reject(new Error(`Couldn't resolve component "${c}" at "${o.path}"`));const d=Dc(f)?f.default:f;o.components[c]=d;const y=(d.__vccOpts||d)[t];return y&&st(y,n,s,o,c,r)()}))}}return i}function Fu(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function Ur(e){const t=He(Kn),n=He(Ks),s=Fe(()=>t.resolve(At(e.to))),r=Fe(()=>{const{matched:l}=s.value,{length:h}=l,f=l[h-1],d=n.matched;if(!f||!d.length)return-1;const p=d.findIndex(Lt.bind(null,f));if(p>-1)return p;const y=Kr(l[h-2]);return h>1&&Kr(f)===y&&d[d.length-1].path!==y?d.findIndex(Lt.bind(null,l[h-2])):p}),i=Fe(()=>r.value>-1&&ju(n.params,s.value.params)),o=Fe(()=>r.value>-1&&r.value===n.matched.length-1&&lo(n.params,s.value.params));function c(l={}){return ku(l)?t[At(e.replace)?"replace":"push"](At(e.to)).catch(Kt):Promise.resolve()}return{route:s,href:Fe(()=>s.value.href),isActive:i,isExactActive:o,navigate:c}}const $u=$s({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:Ur,setup(e,{slots:t}){const n=Fn(Ur(e)),{options:s}=He(Kn),r=Fe(()=>({[Wr(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[Wr(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const i=t.default&&t.default(n);return e.custom?i:Ds("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:r.value},i)}}}),Hu=$u;function ku(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function ju(e,t){for(const n in t){const s=t[n],r=e[n];if(typeof s=="string"){if(s!==r)return!1}else if(!ke(r)||r.length!==s.length||s.some((i,o)=>i!==r[o]))return!1}return!0}function Kr(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Wr=(e,t,n)=>e??t??n,Bu=$s({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=He(Es),r=Fe(()=>e.route||s.value),i=He(Dr,0),o=Fe(()=>{let h=At(i);const{matched:f}=r.value;let d;for(;(d=f[h])&&!d.components;)h++;return h}),c=Fe(()=>r.value.matched[o.value]);_n(Dr,Fe(()=>o.value+1)),_n(Nu,c),_n(Es,r);const l=mn();return yn(()=>[l.value,c.value,e.name],([h,f,d],[p,y,x])=>{f&&(f.instances[d]=h,y&&y!==f&&h&&h===p&&(f.leaveGuards.size||(f.leaveGuards=y.leaveGuards),f.updateGuards.size||(f.updateGuards=y.updateGuards))),h&&f&&(!y||!Lt(f,y)||!p)&&(f.enterCallbacks[d]||[]).forEach(F=>F(h))},{flush:"post"}),()=>{const h=r.value,f=e.name,d=c.value,p=d&&d.components[f];if(!p)return qr(n.default,{Component:p,route:h});const y=d.props[f],x=y?y===!0?h.params:typeof y=="function"?y(h):y:null,k=Ds(p,se({},x,t,{onVnodeUnmounted:I=>{I.component.isUnmounted&&(d.instances[f]=null)},ref:l}));return qr(n.default,{Component:k,route:h})||k}}});function qr(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const Vu=Bu;function _f(e){const t=Pu(e.routes,e),n=e.parseQuery||Iu,s=e.stringifyQuery||Vr,r=e.history,i=jt(),o=jt(),c=jt(),l=Jo(Xe);let h=Xe;Ct&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const f=ss.bind(null,b=>""+b),d=ss.bind(null,tu),p=ss.bind(null,Yt);function y(b,L){let T,B;return uo(b)?(T=t.getRecordMatcher(b),B=L):B=b,t.addRoute(B,T)}function x(b){const L=t.getRecordMatcher(b);L&&t.removeRoute(L)}function F(){return t.getRoutes().map(b=>b.record)}function k(b){return!!t.getRecordMatcher(b)}function I(b,L){if(L=se({},L||l.value),typeof b=="string"){const a=rs(n,b,L.path),g=t.resolve({path:a.path},L),v=r.createHref(a.fullPath);return se(a,g,{params:p(g.params),hash:Yt(a.hash),redirectedFrom:void 0,href:v})}let T;if(b.path!=null)T=se({},b,{path:rs(n,b.path,L.path).path});else{const a=se({},b.params);for(const g in a)a[g]==null&&delete a[g];T=se({},b,{params:d(a)}),L.params=d(L.params)}const B=t.resolve(T,L),ne=b.hash||"";B.params=f(p(B.params));const le=ru(s,se({},b,{hash:Xc(ne),path:B.path})),u=r.createHref(le);return se({fullPath:le,hash:ne,query:s===Vr?Lu(b.query):b.query||{}},B,{redirectedFrom:void 0,href:u})}function M(b){return typeof b=="string"?rs(n,b,l.value.path):se({},b)}function m(b,L){if(h!==b)return Nt(8,{from:L,to:b})}function E(b){return G(b)}function $(b){return E(se(M(b),{replace:!0}))}function P(b){const L=b.matched[b.matched.length-1];if(L&&L.redirect){const{redirect:T}=L;let B=typeof T=="function"?T(b):T;return typeof B=="string"&&(B=B.includes("?")||B.includes("#")?B=M(B):{path:B},B.params={}),se({query:b.query,hash:b.hash,params:B.path!=null?{}:b.params},B)}}function G(b,L){const T=h=I(b),B=l.value,ne=b.state,le=b.force,u=b.replace===!0,a=P(T);if(a)return G(se(M(a),{state:typeof a=="object"?se({},ne,a.state):ne,force:le,replace:u}),L||T);const g=T;g.redirectedFrom=L;let v;return!le&&iu(s,B,T)&&(v=Nt(16,{to:g,from:B}),je(B,B,!0,!1)),(v?Promise.resolve(v):j(g,B)).catch(_=>Ke(_)?Ke(_,2)?_:Ge(_):V(_,g,B)).then(_=>{if(_){if(Ke(_,2))return G(se({replace:u},M(_.to),{state:typeof _.to=="object"?se({},ne,_.to.state):ne,force:le}),L||g)}else _=A(g,B,!0,u,ne);return Z(g,B,_),_})}function H(b,L){const T=m(b,L);return T?Promise.reject(T):Promise.resolve()}function W(b){const L=vt.values().next().value;return L&&typeof L.runWithContext=="function"?L.runWithContext(b):b()}function j(b,L){let T;const[B,ne,le]=Du(b,L);T=is(B.reverse(),"beforeRouteLeave",b,L);for(const a of B)a.leaveGuards.forEach(g=>{T.push(st(g,b,L))});const u=H.bind(null,b,L);return T.push(u),ye(T).then(()=>{T=[];for(const a of i.list())T.push(st(a,b,L));return T.push(u),ye(T)}).then(()=>{T=is(ne,"beforeRouteUpdate",b,L);for(const a of ne)a.updateGuards.forEach(g=>{T.push(st(g,b,L))});return T.push(u),ye(T)}).then(()=>{T=[];for(const a of le)if(a.beforeEnter)if(ke(a.beforeEnter))for(const g of a.beforeEnter)T.push(st(g,b,L));else T.push(st(a.beforeEnter,b,L));return T.push(u),ye(T)}).then(()=>(b.matched.forEach(a=>a.enterCallbacks={}),T=is(le,"beforeRouteEnter",b,L,W),T.push(u),ye(T))).then(()=>{T=[];for(const a of o.list())T.push(st(a,b,L));return T.push(u),ye(T)}).catch(a=>Ke(a,8)?a:Promise.reject(a))}function Z(b,L,T){c.list().forEach(B=>W(()=>B(b,L,T)))}function A(b,L,T,B,ne){const le=m(b,L);if(le)return le;const u=L===Xe,a=Ct?history.state:{};T&&(B||u?r.replace(b.fullPath,se({scroll:u&&a&&a.scroll},ne)):r.push(b.fullPath,ne)),l.value=b,je(b,L,T,u),Ge()}let z;function ue(){z||(z=r.listen((b,L,T)=>{if(!rn.listening)return;const B=I(b),ne=P(B);if(ne){G(se(ne,{replace:!0}),B).catch(Kt);return}h=B;const le=l.value;Ct&&du(Lr(le.fullPath,T.delta),Un()),j(B,le).catch(u=>Ke(u,12)?u:Ke(u,2)?(G(u.to,B).then(a=>{Ke(a,20)&&!T.delta&&T.type===Xt.pop&&r.go(-1,!1)}).catch(Kt),Promise.reject()):(T.delta&&r.go(-T.delta,!1),V(u,B,le))).then(u=>{u=u||A(B,le,!1),u&&(T.delta&&!Ke(u,8)?r.go(-T.delta,!1):T.type===Xt.pop&&Ke(u,20)&&r.go(-1,!1)),Z(B,le,u)}).catch(Kt)}))}let fe=jt(),D=jt(),Y;function V(b,L,T){Ge(b);const B=D.list();return B.length?B.forEach(ne=>ne(b,L,T)):console.error(b),Promise.reject(b)}function Ie(){return Y&&l.value!==Xe?Promise.resolve():new Promise((b,L)=>{fe.add([b,L])})}function Ge(b){return Y||(Y=!b,ue(),fe.list().forEach(([L,T])=>b?T(b):L()),fe.reset()),b}function je(b,L,T,B){const{scrollBehavior:ne}=e;if(!Ct||!ne)return Promise.resolve();const le=!T&&pu(Lr(b.fullPath,0))||(B||!T)&&history.state&&history.state.scroll||null;return vi().then(()=>ne(b,L,le)).then(u=>u&&hu(u)).catch(u=>V(u,b,L))}const Ce=b=>r.go(b);let bt;const vt=new Set,rn={currentRoute:l,listening:!0,addRoute:y,removeRoute:x,hasRoute:k,getRoutes:F,resolve:I,options:e,push:E,replace:$,go:Ce,back:()=>Ce(-1),forward:()=>Ce(1),beforeEach:i.add,beforeResolve:o.add,afterEach:c.add,onError:D.add,isReady:Ie,install(b){const L=this;b.component("RouterLink",Hu),b.component("RouterView",Vu),b.config.globalProperties.$router=L,Object.defineProperty(b.config.globalProperties,"$route",{enumerable:!0,get:()=>At(l)}),Ct&&!bt&&l.value===Xe&&(bt=!0,E(r.location).catch(ne=>{}));const T={};for(const ne in Xe)Object.defineProperty(T,ne,{get:()=>l.value[ne],enumerable:!0});b.provide(Kn,L),b.provide(Ks,ai(T)),b.provide(Es,l);const B=b.unmount;vt.add(b),b.unmount=function(){vt.delete(b),vt.size<1&&(h=Xe,z&&z(),z=null,l.value=Xe,bt=!1,Y=!1),B()}}};function ye(b){return b.reduce((L,T)=>L.then(()=>W(T)),Promise.resolve())}return rn}function Du(e,t){const n=[],s=[],r=[],i=Math.max(t.matched.length,e.matched.length);for(let o=0;oLt(h,c))?s.push(c):n.push(c));const l=e.matched[o];l&&(t.matched.find(h=>Lt(h,l))||r.push(l))}return[n,s,r]}function bf(){return He(Kn)}function vf(){return He(Ks)}const Ef=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n};export{sf as $,yn as A,Qi as B,vi as C,Po as D,Ku as E,Jo as F,Qu as G,_n as H,ks as I,J,hf as K,vf as L,zu as M,ae as N,ve as O,Xu as P,nf as Q,Gi as R,Wu as S,Xi as T,Wi as U,ll as V,tc as W,ff as X,af as Y,Ju as Z,Ef as _,hi as a,df as a0,tf as a1,of as a2,mf as a3,_f as a4,Xe as a5,lf as a6,yf as a7,Vu as a8,qu as a9,Gu as aa,mt as ab,ee as ac,rf as ad,Fn as b,q as c,Yu as d,cf as e,gf as f,$s as g,Fe as h,he as i,Ds as j,uf as k,pf as l,He as m,Mi as n,Hs as o,Ui as p,ef as q,mn as r,Zu as s,zi as t,bf as u,Uu as v,As as w,Rs as x,Ee as y,At as z};
diff --git a/assets/getting-started.html-010fb6e6.js b/assets/getting-started.html-010fb6e6.js
new file mode 100644
index 000000000..aef8e87b8
--- /dev/null
+++ b/assets/getting-started.html-010fb6e6.js
@@ -0,0 +1 @@
+import{_ as e,p as a,q as n,R as t,t as o}from"./framework-c782e227.js";const r={},s=t("p",null,[o("Moved to "),t("a",{href:"./getting-started"},"Getting Started")],-1),c=[s];function _(d,i){return a(),n("div",null,c)}const f=e(r,[["render",_],["__file","getting-started.html.vue"]]);export{f as default};
diff --git a/assets/getting-started.html-d424ee3b.js b/assets/getting-started.html-d424ee3b.js
new file mode 100644
index 000000000..a0d779f3b
--- /dev/null
+++ b/assets/getting-started.html-d424ee3b.js
@@ -0,0 +1 @@
+const t=JSON.parse('{"key":"v-5dc4b15a","path":"/getting-started.html","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/getting started.png"}],["meta",{"name":"og:description","content":")"}]],"description":")"},"headers":[],"git":{},"filePathRelative":"getting-started.md"}');export{t as data};
diff --git a/assets/github-star-ecc4877c.js b/assets/github-star-ecc4877c.js
new file mode 100644
index 000000000..98fc624d0
--- /dev/null
+++ b/assets/github-star-ecc4877c.js
@@ -0,0 +1 @@
+import{_ as r}from"./app-8ffcfbb5.js";import{g as c,ab as p,j as n,ac as h,_ as u,M as d,p as l,U as f,V as _,t as g}from"./framework-c782e227.js";const m=c({name:"github-button",props:{href:String,ariaLabel:String,title:String,dataIcon:String,dataColorScheme:String,dataSize:String,dataShowCount:String,dataText:String},render:function(){const t={ref:"_"};for(const e in this.$props)t[p(e)]=this.$props[e];return n("span",[h(this.$slots,"default")?n("a",t,this.$slots.default()):n("a",t)])},mounted:function(){this.paint()},beforeUpdate:function(){this.reset()},updated:function(){this.paint()},beforeUnmount:function(){this.reset()},methods:{paint:function(){const t=this.$el.appendChild(document.createElement("span")),e=this;r(()=>import("./buttons.esm-48f94bc9.js"),[]).then(function(o){o.render(t.appendChild(e.$refs._),function(a){try{t.parentNode.replaceChild(a,t)}catch{}})})},reset:function(){this.$el.replaceChild(this.$refs._,this.$el.lastChild)}}}),b={components:{GithubButton:m}};function S(t,e,o,a,s,$){const i=d("github-button");return l(),f(i,{href:"https://github.com/needle-tools/needle-engine-support","data-icon":"octicon-star","data-size":"large","data-show-count":"true","aria-label":"Star needle-tools/needle-engine-support on GitHub"},{default:_(()=>[g("Star")]),_:1})}const v=u(b,[["render",S],["__file","github-star.vue"]]);export{v as default};
diff --git a/assets/html.html-cfcd67e7.js b/assets/html.html-cfcd67e7.js
new file mode 100644
index 000000000..c6cf64b6a
--- /dev/null
+++ b/assets/html.html-cfcd67e7.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-e554bd96","path":"/html.html","title":"Frameworks, Bundlers, HTML","lang":"en-US","frontmatter":{"title":"Frameworks, Bundlers, HTML","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/frameworks, bundlers, html.png"}],["meta",{"name":"og:description","content":"---\\nNeedle Engine is build as a web component.\\nThis means just install @needle-tools/engine in your project and include anywhere in your web-project."}]],"description":"---\\nNeedle Engine is build as a web component.\\nThis means just install @needle-tools/engine in your project and include anywhere in your web-project."},"headers":[{"level":2,"title":"Bundling and web frontends","slug":"bundling-and-web-frontends","link":"#bundling-and-web-frontends","children":[{"level":3,"title":"Vite, Vue, React, Svelte, React Three Fiber...","slug":"vite-vue-react-svelte-react-three-fiber...","link":"#vite-vue-react-svelte-react-three-fiber...","children":[]},{"level":3,"title":"Tree-shaking to reduce bundle size","slug":"tree-shaking-to-reduce-bundle-size","link":"#tree-shaking-to-reduce-bundle-size","children":[]}]},{"level":2,"title":"Creating a PWA","slug":"creating-a-pwa","link":"#creating-a-pwa","children":[]},{"level":2,"title":"Accessing Needle Engine and Components from external javascript","slug":"accessing-needle-engine-and-components-from-external-javascript","link":"#accessing-needle-engine-and-components-from-external-javascript","children":[]},{"level":2,"title":"Customizing how loading looks","slug":"customizing-how-loading-looks","link":"#customizing-how-loading-looks","children":[{"level":3,"title":"Builtin styles","slug":"builtin-styles","link":"#builtin-styles","children":[]},{"level":3,"title":"Custom Loading Style โ PRO feature","slug":"custom-loading-style-pro-feature","link":"#custom-loading-style-pro-feature","children":[]}]}],"git":{},"filePathRelative":"html.md"}');export{e as data};
diff --git a/assets/html.html-efd3b0c8.js b/assets/html.html-efd3b0c8.js
new file mode 100644
index 000000000..4ad942e37
--- /dev/null
+++ b/assets/html.html-efd3b0c8.js
@@ -0,0 +1,22 @@
+import{_ as r}from"./custom-loading-style-65a23c0d.js";import{_ as c,M as l,p,q as d,R as e,t as n,N as t,V as o,a1 as i}from"./framework-c782e227.js";const u="/docs/imgs/unity-needle-engine-modules-physics.jpg",h={},m=i('
Needle Engine is build as a web component. This means just install @needle-tools/engine in your project and include <needle-engine src="path/to/your.glb"> anywhere in your web-project.
Install using npm: npm i @needle-tools/engine
With our default Vite based project template Needle Engine gets bundled into a web app on deployment. This ensures smaller files, tree-shaking (similar to code stripping in Unity) and optimizes load times. Instead of downloading numerous small scripts and components, only one or a few are downloaded that contain the minimal code needed.
',4),g={href:"https://vitejs.dev/guide/why.html",target:"_blank",rel:"noopener noreferrer"},k=e("h3",{id:"vite-vue-react-svelte-react-three-fiber...",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#vite-vue-react-svelte-react-three-fiber...","aria-hidden":"true"},"#"),n(" Vite, Vue, React, Svelte, React Three Fiber...")],-1),f={href:"https://vitejs.dev",target:"_blank",rel:"noopener noreferrer"},b=e("p",null,"Here's some example tech stacks that are possible and that we use Needle Engine with:",-1),v=e("li",null,[e("p",null,[e("strong",null,"Vite + HTML"),n(" โ It is what our default template uses")])],-1),_=e("strong",null,"Vite + Vue",-1),w={href:"https://needle.tools",target:"_blank",rel:"noopener noreferrer"},y={href:"https://github.com/needle-tools/needle-engine-samples",target:"_blank",rel:"noopener noreferrer"},j=e("li",null,[e("p",null,[e("strong",null,"Vite + Svelte")])],-1),x=e("li",null,[e("p",null,[e("strong",null,"Vite + SvelteKit")])],-1),P=e("li",null,[e("p",null,[e("strong",null,"Vite + React"),n(" โ There's an experimental template shipped with the Unity integration for this that you can pick when generating a project!")])],-1),E=e("li",null,[e("p",null,[e("strong",null,"react-three-fiber"),n(" โ There's an experimental template shipped with the Unity integration for this that you can pick when generating a project!")])],-1),T=e("strong",null,"Vercel & Nextjs",-1),V={href:"https://github.com/needle-engine/nextjs-sample",target:"_blank",rel:"noopener noreferrer"},A=e("strong",null,"CDN without any bundler",-1),N=e("p",null,[n("In short: we're currently providing a minimal vite template, but you can extend it or switch to other frameworks โ"),e("br"),n(" Let us know what and how you build, and how we can improve the experience for your usecase or provide an example!")],-1),W={class:"custom-container tip"},R=e("p",{class:"custom-container-title"},"TIP",-1),S=e("code",null,"needle.config.json",-1),I=e("code",null,"baseUrl",-1),C=i('How do I create a custom project template in Unity?
You can create and share your own web project templates to use other bundlers, build systems, or none at all.
Create a new Template
Select Create/Needle Engine/Project Template to add a ProjectTemplate into the folder you want to use as a template
Done! It's that simple.
The dependencies come from unity when there is a NpmDef in the project (so when your project uses local references). You could also publish your packages to npm and reference them via version number.
We support creating a Progressive Web App (PWA) from your vite based project.
',3),z={href:"https://vite-pwa-org.netlify.app/",target:"_blank",rel:"noopener noreferrer"},q=e("code",null,"npm install vite-plugin-pwa --save-dev",-1),D=e("li",null,[n("Modify "),e("code",null,".vite.config.js"),n(" and pass the options both to the "),e("code",null,"needlePlugins"),n(" as well as the "),e("code",null,"VitePWA"),n(" method (see code below)")],-1),U=i(`
import{ VitePWA }from'vite-plugin-pwa';
+
+exportdefaultdefineConfig(async({ command })=>{
+
+ // create the PWAOptions object.
+ // You can edit or enter PWA settings here (e.g. change the PWA name or add icons).
+ /** @type{import("vite-plugin-pwa").VitePWAOptions} */
+ const PWAOptions ={};
+
+ const{ needlePlugins }=awaitimport("@needle-tools/engine/plugins/vite/index.js");
+
+ return{
+ plugins:[
+ // pass the PWAOptions object to the needlePlugins and the VitePWA function
+ needlePlugins(command, needleConfig,{pwaOptions: PWAOptions }),
+ VitePWA(PWAOptions),
+ ],
+ // the rest of your vite config...
+
# Accessing Needle Engine and Components from external javascript
`,2),B=e("br",null,null,-1),M=e("h2",{id:"customizing-how-loading-looks",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#customizing-how-loading-looks","aria-hidden":"true"},"#"),n(" Customizing how loading looks")],-1),F=e("em",null,"Loading Display",-1),H=i('
The needle-engine loading appearance can use a light or dark skin. To change the appearance use the loading-style attribute on the <needle-engine> web component. Options are light and dark (default):
',4),Y=e("em",null,"Loading Display",-1),K=e("p",null,[e("img",{src:r,alt:"custom loading"})],-1);function G(J,$){const s=l("ExternalLinkIcon"),a=l("RouterLink");return p(),d("div",null,[m,e("p",null,[n("Vite (our default bundler) has a good explanation why web apps should be bundled: "),e("a",g,[n("Why Bundle for Production"),t(s)])]),k,e("p",null,[n("Needle Engine is unoponiated about the choice of framework. The default template only uses vite as bundler. Adding vue to that is easy (see the "),e("a",f,[n("vite docs"),t(s)]),n("), we also provide an (experimental) react-three-fiber template and there should be nothing stopping your from using simpler or more complex frameworks.")]),b,e("ul",null,[v,e("li",null,[e("p",null,[_,n(" โ This is what the "),e("a",w,[n("Needle Tools"),t(s)]),n(" website uses!. Find a sample to download "),e("a",y,[n("here"),t(s)]),n(".")])]),j,x,P,E,e("li",null,[e("p",null,[T,n(" โ Find a "),e("a",V,[n("example nextjs project here"),t(s)])])]),e("li",null,[e("p",null,[A,n(" โ Find a code example "),t(a,{to:"/vanilla-js.html"},{default:o(()=>[n("here")]),_:1})])])]),N,e("div",W,[R,e("p",null,[n("Some frameworks require custom settings in "),S,n(". Learn more "),t(a,{to:"/reference/needle-config-json.html"},{default:o(()=>[n("here")]),_:1}),n(". Typically, the "),I,n(" needs to be set.")])]),C,e("p",null,[n("Tree shaking refers to a common practice when it comes to bundling of web applications ("),e("a",L,[n("see MSDN docs"),t(s)]),n("). It means that code paths and features that are not used in your code will be removed from the final bundled javascript file(s) to reduce filesize. See below about features that Needle Engine includes and remove them:")]),O,e("ol",null,[e("li",null,[n("Install the "),e("a",z,[n("Vite PWA plugin"),t(s)]),n(" in your web project: "),q]),D]),U,e("p",null,[n("Code that you expose can be accessed from JavaScript after bundling. This allows to build viewers and other applications where there's a split between data known at edit time and data only known at runtime (e.g. dynamically loaded files, user generated content)."),B,n(" For accessing components from regular javascript outside of the engine please refer to the "),t(a,{to:"/scripting.html#accessing-needle-engine-and-components-from-anywhere"},{default:o(()=>[n("interop with regular javascript section")]),_:1})]),M,e("p",null,[n("See the "),F,n(" section in "),t(a,{to:"/reference/needle-engine-attributes.html"},{default:o(()=>[n("needle engine component reference")]),_:1})]),H,e("p",null,[n("Please see the "),Y,n(" section in "),t(a,{to:"/reference/needle-engine-attributes.html"},{default:o(()=>[n("needle engine component reference")]),_:1})]),K])}const Z=c(h,[["render",G],["__file","html.html.vue"]]);export{Z as default};
diff --git a/assets/index-70769223.js b/assets/index-70769223.js
new file mode 100644
index 000000000..6d142a25e
--- /dev/null
+++ b/assets/index-70769223.js
@@ -0,0 +1 @@
+var v=function(){return Boolean(window.location.hostname==="localhost"||window.location.hostname==="[::1]"||window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/))},c;typeof window<"u"&&(typeof Promise<"u"?c=new Promise(function(t){return window.addEventListener("load",t)}):c={then:function(t){return window.addEventListener("load",t)}});function s(t,n){n===void 0&&(n={});var i=n.registrationOptions;i===void 0&&(i={}),delete n.registrationOptions;var e=function(r){for(var f=[],a=arguments.length-1;a-- >0;)f[a]=arguments[a+1];n&&n[r]&&n[r].apply(n,f)};"serviceWorker"in navigator&&c.then(function(){v()?(l(t,e,i),navigator.serviceWorker.ready.then(function(r){e("ready",r)}).catch(function(r){return o(e,r)})):(u(t,e,i),navigator.serviceWorker.ready.then(function(r){e("ready",r)}).catch(function(r){return o(e,r)}))})}function o(t,n){navigator.onLine||t("offline"),t("error",n)}function u(t,n,i){navigator.serviceWorker.register(t,i).then(function(e){if(n("registered",e),e.waiting){n("updated",e);return}e.onupdatefound=function(){n("updatefound",e);var r=e.installing;r.onstatechange=function(){r.state==="installed"&&(navigator.serviceWorker.controller?n("updated",e):n("cached",e))}}}).catch(function(e){return o(n,e)})}function l(t,n,i){fetch(t).then(function(e){e.status===404?(n("error",new Error("Service worker not found at "+t)),d()):e.headers.get("content-type").indexOf("javascript")===-1?(n("error",new Error("Expected "+t+" to have javascript content-type, but received "+e.headers.get("content-type"))),d()):u(t,n,i)}).catch(function(e){return o(n,e)})}function d(){"serviceWorker"in navigator&&navigator.serviceWorker.ready.then(function(t){t.unregister()}).catch(function(t){return o(emit,t)})}export{s as register,d as unregister};
diff --git a/assets/index.html-01c53aa7.js b/assets/index.html-01c53aa7.js
new file mode 100644
index 000000000..c62d1f3d9
--- /dev/null
+++ b/assets/index.html-01c53aa7.js
@@ -0,0 +1,28 @@
+import{_ as a,M as t,p as e,q as p,N as o,R as n,a1 as c}from"./framework-c782e227.js";const i={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=c(`
Example for adding custom USDZ behaviours for iOS AR
`,3);function r(k,d){const s=t("contribution-header");return e(),p("div",null,[l,o(s,{url:"https://github.com/marwie",author:"marwie",page:"/docs/community/contributions/marwie",profileImage:"https://avatars.githubusercontent.com/u/5083203?s=100&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/157",title:"Everywhere Action: Emphasize on Click",gradient:"True"}),u])}const v=a(i,[["render",r],["__file","index.html.vue"]]);export{v as default};
diff --git a/assets/index.html-0784c019.js b/assets/index.html-0784c019.js
new file mode 100644
index 000000000..a7aadf632
--- /dev/null
+++ b/assets/index.html-0784c019.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-07e00b06","path":"/community/contributions/krisrok/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/contributions: krisrok.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-0991b7d8.js b/assets/index.html-0991b7d8.js
new file mode 100644
index 000000000..21829847d
--- /dev/null
+++ b/assets/index.html-0991b7d8.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-1b71f33d","path":"/community/contributions/web3kev/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/contributions: web3kev.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-0a16bdd2.js b/assets/index.html-0a16bdd2.js
new file mode 100644
index 000000000..22955d28b
--- /dev/null
+++ b/assets/index.html-0a16bdd2.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-4fe6a858","path":"/community/contributions/kipash/calculate-pointer-world-position/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/kipash: calculate pointer world position.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-0a61ccc5.js b/assets/index.html-0a61ccc5.js
new file mode 100644
index 000000000..b251979c6
--- /dev/null
+++ b/assets/index.html-0a61ccc5.js
@@ -0,0 +1,164 @@
+import{_ as a,M as t,p as e,q as p,N as o,R as n,a1 as c}from"./framework-c782e227.js";const i={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=c(`
In a multiuser session, typically objects are instantiated using instantiateSynced as such:
My particular use-case was for generating programmatically a random scene made of cubes, and that scene had to be the same for all users of the same room. I had used the example above but for some unknown reasons sometimes the scenes were partially rendered when instantiating simultaneously >400 objects. @Marcel of Needle suggested to generate a seed (position of all objects in the scene) and send that seed instead using :
this.context.connection.send()
+
All users using :
this.context.connection.beginListen()
+
would receive any seed previously sent, upon joining the same room, allowing them to instantiate cubes according to that seed (array of Vector3).
Here is a script illustrating the use of the send method and the beginListen counterpart:
+//This is an example of sending the seed of a randomly generated scene made of cubes, for all other instances logging into the same room to create the same scene.
+
+//This script requires a prefab (e.g. a 1x1x1 Cube)
+//This script will generate and build randomly positioned cubes (random walk) as a child of the object it is attached to.
+//The generateSeed() method is in this script called via a button. The button is deactivated once the seed has been transmitted.
+//Any users joining the same room will receive the seed and build the exact same scene
+
+
+import{ Behaviour,GameObject,serializable,InstantiateOptions}from"@needle-tools/engine";
+import{ Vector3, Object3D }from"three";
+
+
+exportclassNetworkedSeedextendsBehaviour
+{
+ @serializable(Object3D)
+ prefab?: GameObject;
+
+ @serializable(Object3D)
+ generateButton?: Object3D;
+
+ public seedSize:number=30;
+
+ seed: Vector3[]=[];
+
+ onEnable():void{
+ this.context.connection.beginListen("mySeed",this.onDataReceived);
+ if(this.generateButton)
+ {
+ this.generateButton.visible=true;
+ }
+ }
+ onDisable():void{
+ this.context.connection.stopListen("mySeed",this.onDataReceived);
+ }
+
+ onDataReceived=(data:any)=>{
+
+ console.log("Received data:", data.mySeed);
+ if(this.seed.length===0)
+ {
+ //prevent other generations of the seed
+ if(this.generateButton)
+ {
+ this.generateButton.visible=false;
+ }
+
+ this.seed=data.mySeed;
+ //build scene
+ this.buildScene();
+ }
+ };
+
+
+ //generate and send seed to all from the button generateButton
+ publicgenerateSeed():void{
+
+ if(this.seed.length==0)//no seed found => generate one
+ {
+ this.seed =[];
+ const uniquePositions =newSet<string>();
+
+ //start at origin
+ const startPosition =newVector3(0,0,0);
+ this.seed.push(startPosition.clone());
+ uniquePositions.add(startPosition.toArray().toString());
+
+ //go for a random walk of length : seedSize
+ while(this.seed.length <this.seedSize){
+ const lastPosition =this.seed[this.seed.length -1];
+ let newPosition: Vector3;
+
+ //walk and add position, making sure they are unique
+ do{
+ const direction =this.getRandomDirection();
+ newPosition = lastPosition.clone().add(direction);
+ }while(uniquePositions.has(newPosition.toArray().toString()));
+
+ this.seed.push(newPosition.clone());
+ uniquePositions.add(newPosition.toArray().toString());
+ }
+
+ //send the seed to all on the server
+ this.sendSeed();
+
+ //prevent other generations of the seed
+ if(this.generateButton)
+ {
+ this.generateButton.visible=false;
+ }
+ }
+
+ //build scene locally
+ this.buildScene();
+ }
+
+ privatesendSeed():void{
+ if(this.seed.length!=0)
+ {
+ this.context.connection.send("mySeed",{guid:this.guid, mySeed:this.seed});
+ console.log("------ SEED SENT -------");
+ }
+ }
+
+ publicbuildScene():void{
+
+ //check if the seed is not empty
+ if(this.seed.length==0)
+ {
+ console.log("array was empty");
+ return;
+ }
+
+ //check if the scene has already been built
+ if(this.gameObject.children.length>0)
+ {
+ console.log("Scene already present");
+ return;
+ }
+
+ // Create cubes at each position of the random walk
+ for(let i=0; i<this.seed.length; i++)
+ {
+ const option =newInstantiateOptions();
+ option.context =this.context;
+ option.parent=this.gameObject;
+ option.position =this.seed[i];
+
+ if(this.prefab!=null)
+ {
+ const cube = GameObject.instantiate(this.prefab, option)as GameObject;
+ }
+ }
+
+ console.log("----------- Scene Built ---------");
+
+ }
+
+ privategetRandomDirection(): Vector3 {
+ const x = Math.random()<0.5?-1:1;
+ const y = Math.random()<0.5?-1:1;
+ const z = Math.random()<0.5?-1:1;
+ returnnewVector3(x, y, z);
+ }
+
+}
+
The above script is placed on an object (any Transform) and will generate an array of unique Vector3 positions for a specified length (seedSize) after generateSeed() is called (In this case it is called from a button: generateButton).
Once generated it will send the array to the server and build the scene. The building process consist of instantiating the prefab at each Vector3 position of the seed (this.seed) array.
Any user joining the same room after a seed has been generated and sent, will receive the seed from the server and trigger the callback onDataReceived() which will cache the seed array, disable the button, and build the scene with the prefab, according to the seed.
This gives a way to generate a scene and communicate the seed of that scene, for each user to build locally.
This was the solution I chose which worked better than instantiating a complex scene (>400 objects) with instantiateSynced which would occasionally cause bugs.
`,14);function k(r,d){const s=t("contribution-header");return e(),p("div",null,[l,o(s,{url:"https://github.com/Web3Kev",author:"Web3Kev",page:"/docs/community/contributions/web3kev",profileImage:"https://avatars.githubusercontent.com/u/106066970?s=100&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/153",title:"Network instantiation of multiple objects",gradient:"True"}),u])}const m=a(i,[["render",k],["__file","index.html.vue"]]);export{m as default};
diff --git a/assets/index.html-0bfcaea5.js b/assets/index.html-0bfcaea5.js
new file mode 100644
index 000000000..64281c8be
--- /dev/null
+++ b/assets/index.html-0bfcaea5.js
@@ -0,0 +1,250 @@
+import{_ as a,M as t,p as e,q as p,N as o,R as n,a1 as c}from"./framework-c782e227.js";const l={},i=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=c(`
The following code enables you to use both controllers in VR (tested on Quest) and scale the player's perspective (XRRig) by squeezing the grab triggers and moving the controllers closer (pinch out) or further apart (pinch in). The boolean allowWorldScaling has to be ticked in unity for that to work.
Upon selecting a draggable object (Drag controls script), the player can scale up or down that object, while keeping the finger on the trigger and squeezing both grab buttons and moving the hands closer or apart.
The current script enables you to visually see the scale. Create a world canvas with a text component as a child. Assign the world canvas to scaleTextObject and the text to scaleText. scaleTextObject will then spawn in front of the player and follow the head movement whenever scaling.
At the moment the position of the hands (controllers) is done by finding the avatar's hands. I couldn't make it work otherwise. If you find a better way please share.
import{ Behaviour, WebXR,serializeable, WebXREvent,WebXRAvatar,GameObject, AvatarMarker,Text}from"@needle-tools/engine";
+import{ Object3D, Vector3,Quaternion,PerspectiveCamera}from"three";
+
+exportclassSqueezeScaleextendsBehaviour{
+
+
+ private webXR?: WebXR;
+
+ private selectedObj: Object3D|null=null;
+
+ @serializeable(Object3D)
+ scaleTextObject: Object3D|null=null;
+
+ @serializeable(Text)
+ scaleText?: Text;
+
+ public allowWorldScaling:boolean=false;
+
+ private leftSqueeze:boolean=false;
+ private rightSqueeze:boolean=false;
+
+ private bothSqueezeStarted=false;
+
+ private rigScaleUpdated=false;
+
+ private initialDistance:number=1;
+ private initialScale:number=1;
+ private newScale:number|null=null;
+
+ private leftHand?:Object3D;
+ private rightHand?:Object3D;
+ private head?:Object3D;
+
+ start():void{
+
+ let _webxr=GameObject.findObjectOfType(WebXR);
+ if(_webxr)
+ {
+ this.webXR=_webxr;
+ console.log("webxr found");
+ }
+
+ //Wait for XR Session
+ WebXR.addEventListener(WebXREvent.XRStarted,()=>{
+ //listen to squeeze events
+ this.context.xrSession?.addEventListener("squeezestart",(event)=>{this.onSqueezeEvent(event,true);});
+ this.context.xrSession?.addEventListener("squeezeend",(event)=>{this.onSqueezeEvent(event,false);});
+ });
+ }
+
+
+ onSqueezeEvent(event: XRInputSourceEvent, status:boolean){
+
+ if(event.inputSource.handedness==="right")
+ {
+ this.rightSqueeze=status;
+ }
+
+ if(event.inputSource.handedness==="left")
+ {
+ this.leftSqueeze=status;
+ }
+ }
+
+ update(){
+
+ if(this.context.isInVR)
+ {
+ //cache object selected if any
+ this.objectGrab();
+
+ //if both grips are squeezed
+ if(this.leftSqueeze &&this.rightSqueeze)
+ {
+ //if object is selected either in the left or right controller (only one)
+ if(this.selectedObj!=null)
+ {
+ //after initial distance value has been set
+ if(this.bothSqueezeStarted)
+ {
+ //get current distance between controllers
+ const scaleValue=this.calculateDistance();
+
+ //get distance change since beginning of squeeze to get a "pinch in/out" effect
+ this.newScale=this.initialScale+scaleValue-this.initialDistance;
+
+ //avoid 0 and negative scales
+ if(this.newScale<0.001){this.newScale=0.001;}
+
+ // scale object according to new distance since initial distance
+ this.selectedObj.scale.x=this.newScale;
+ this.selectedObj.scale.y=this.newScale;
+ this.selectedObj.scale.z=this.newScale;
+
+ this.showVisual(this.newScale,"Object :");
+ }
+ else
+ {
+ //get initial distance value (only once at a new squeeze both hands event)
+ this.bothSqueezeStarted=true;
+
+ this.initialDistance=this.calculateDistance();
+
+ //cache object's initial scale
+ this.initialScale=this.selectedObj.scale.x;
+ }
+ }
+ else
+ {
+ //scale world ?
+ if(this.webXR?.Rig &&this.allowWorldScaling)
+ {
+ //after initial distance value has been set
+ if(this.bothSqueezeStarted)
+ {
+ //get current distance between controllers
+ const scaleValue=this.calculateDistance();
+
+ //get distance change since beginning of squeeze to get a "pinch in/out" effect
+ this.newScale=this.initialScale+scaleValue-this.initialDistance;
+
+ //avoid 0 and negative scales
+ if(this.newScale<0.001){this.newScale=0.001;}
+
+ this.showVisual(this.newScale,"World :");
+
+ this.rigScaleUpdated=true;
+ }
+ else
+ {
+ //get initial distance value (only once at a new squeeze both hands event)
+ this.bothSqueezeStarted=true;
+
+ this.initialDistance=this.calculateDistance();
+
+ //cache object's initial scale
+ this.initialScale=this.webXR.Rig.scale.x;
+ }
+ }
+ }
+
+ }
+ else
+ {
+ //reset values
+ this.bothSqueezeStarted=false;
+
+ //if world has been scaled, scale rig accordingly at the end of squeezing and once only
+ if(this.webXR?.Rig &&this.rigScaleUpdated &&this.newScale)
+ {
+ //change rig scale
+ this.webXR.Rig.scale.set(this.newScale,this.newScale,this.newScale);
+ this.webXR.Rig.updateMatrixWorld();
+
+ const cam =this.context.mainCamera as PerspectiveCamera;
+ cam.near=this.newScale>2?0.0001:0.2;
+ cam.updateProjectionMatrix();
+
+ //reset
+ this.rigScaleUpdated=false;
+ }
+
+ if(this.scaleTextObject)
+ {
+ this.scaleTextObject.visible=false;
+ }
+
+ this.newScale=null;
+ }
+ }
+ }
+
+ privatecalculateDistance():number
+ {
+ let distance=1;
+
+ if(this.leftHand &&this.rightHand)
+ {
+
+ const left=this.leftHand.position;
+ const right=this.rightHand.position;
+
+ // Calculate the difference between the positions
+ const dx = left.x - right.x;
+ const dy = left.y - right.y;
+ const dz = left.z - right.z;
+
+ // Calculate the distance using the Euclidean distance formula
+ distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
+ }
+ else
+ {
+ //set positions of controllers from your avatar (only once)
+ let allAvatars = AvatarMarker.instances;
+ if(allAvatars.length>0)
+ {
+ for(let i=0;i<allAvatars.length;i++)
+ {
+ if(allAvatars[i].isLocalAvatar())
+ {
+ const av=allAvatars[i].avatar as WebXRAvatar;
+ if(av!=null)
+ {
+ this.leftHand=av.handLeft as Object3D;
+ this.rightHand=av.handRight as Object3D;
+ this.head = av.head as Object3D;
+ }
+ }
+ }
+ }
+ }
+
+ return distance;
+ }
+
+ showVisual(scale:number, mesg:string):void
+ {
+ if(this.scaleTextObject &&this.head &&this.scaleText)
+ {
+ this.scaleTextObject.visible=true;
+
+ const offset =newVector3(0,0,7);
+ offset.applyQuaternion(this.head.quaternion);
+
+ this.scaleTextObject.position.copy(this.head.position.add(offset));
+
+ const roundedNum=+scale.toFixed(3);
+ this.scaleText.text=mesg+" "+roundedNum;
+ }
+
+ }
+
+
+ objectGrab():void
+ {
+ if(this.webXR?.RightController?.grabbed?.selected)
+ {
+ this.selectedObj =this.webXR.RightController.grabbed.selected;
+ }
+ elseif(this.webXR?.LeftController?.grabbed?.selected)
+ {
+ this.selectedObj =this.webXR.LeftController.grabbed.selected;
+ }
+ else
+ {
+ this.selectedObj=null;
+ }
+ }
+}
+
`,5);function k(r,d){const s=t("contribution-header");return e(),p("div",null,[i,o(s,{url:"https://github.com/Web3Kev",author:"Web3Kev",page:"/docs/community/contributions/web3kev",profileImage:"https://avatars.githubusercontent.com/u/106066970?s=100&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/159",title:"Squeeze to Scale (Object or World) in VR",gradient:"True"}),u])}const b=a(l,[["render",k],["__file","index.html.vue"]]);export{b as default};
diff --git a/assets/index.html-1a7264fe.js b/assets/index.html-1a7264fe.js
new file mode 100644
index 000000000..4e005d44f
--- /dev/null
+++ b/assets/index.html-1a7264fe.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-74ff5ee0","path":"/community/contributions/kipash/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/contributions: kipash.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-1ac4d74c.js b/assets/index.html-1ac4d74c.js
new file mode 100644
index 000000000..9d77d2891
--- /dev/null
+++ b/assets/index.html-1ac4d74c.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-51c63c84","path":"/community/contributions/web3kev/network-instantiation-of-multiple-objects/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/web3kev: network instantiation of multiple objects.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-25a182d2.js b/assets/index.html-25a182d2.js
new file mode 100644
index 000000000..f840a9a28
--- /dev/null
+++ b/assets/index.html-25a182d2.js
@@ -0,0 +1,17 @@
+import{_ as a,M as e,p as t,q as o,N as p,R as n,a1 as i}from"./framework-c782e227.js";const c={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=i(`
This is mostly a basic example on how to contribute. It will then be added on our documentation contributions page: https://engine.needle.tools/docs/community/contributions
Please include at least one code snippet, for example like this:
`,3);function r(d,k){const s=e("contribution-header");return t(),o("div",null,[l,p(s,{url:"https://github.com/marwie",author:"marwie",page:"/docs/community/contributions/marwie",profileImage:"https://avatars.githubusercontent.com/u/5083203?s=100&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/146",title:"Code Contribution Example",gradient:"True"}),u])}const v=a(c,[["render",r],["__file","index.html.vue"]]);export{v as default};
diff --git a/assets/index.html-25fb6b8f.js b/assets/index.html-25fb6b8f.js
new file mode 100644
index 000000000..7b133ae2e
--- /dev/null
+++ b/assets/index.html-25fb6b8f.js
@@ -0,0 +1 @@
+import{_ as d,M as i,p as c,q as h,N as o,V as s,t,Q as u,a1 as p,R as e}from"./framework-c782e227.js";const g="/docs/imgs/banner.webp",m={},_=p('
Needle Engine is a web engine for complex and simple 3D applications alike. Run on your machine and deploy anywhere. It is flexible, extensible and has built-in support for collaboration and XR! It is built around the glTF standard for 3D assets. Powerful integrations for Unity and Blender allow artists and developers to collaborate and manage web applications inside battle-tested 3d editors. Needle Engine Integrations allow you to use Editor features for exporting models, author materials, animate and sequence animations, bake lightmaps and more with ease. Our powerful compression and optimization pipeline for the web make sure your files are ready, small and load fast!
',2),f=e("div",null,[t("Unbelievable Unity editor integration by an order of magnitude,"),e("br"),t("and as straightforward as the docs claim. Wow. โ Chris Mahoney")],-1),w=e("div",null,"This is the best thing I have seen after cinemachine in Unity โ Rinesh Thomas",-1),b=e("div",null,[t("Spent the last 2.5 months building this game, never built a game or used Unity before"),e("br"),t("but absolutely loving the whole process. So rapid! โ Matthew Pieri")],-1),y=e("div",null,[e("a",{href:"https://needle.tools"},"needle.tools"),t(" is a wonderful showcase of what Needle contributes to 3D via the web. I just love it. โ Kevin Curry")],-1),v=e("div",null,"Played with this a bit this morning ๐คฏ๐คฏ pretty magical โ Brit Gardner",-1),x=e("div",null,"This is huge for WebXR and shared, immersive 3D experiences! The AR part worked flawlessly on my Samsung S21. โ Marc Wakefield",-1),N=e("div",null,"This is amazing and if you are curious about WebXR with Unity this will help us get there โ Dilmer Valecillos",-1),k=e("div",null,"We just gotta say WOW ๐คฉ โ Unity for Digital Twins",-1),B=e("br",null,null,-1),T=e("br",null,null,-1),W=e("p",null,[e("a",{href:"https://www.npmjs.com/package/@needle-tools/engine"},[e("img",{src:"https://img.shields.io/npm/v/@needle-tools/engine?style=flat&colorA=000000&colorB=000000"})])],-1),R=e("p",null,[e("a",{href:"https://www.npmjs.com/package/@needle-tools/engine"},[e("img",{src:"https://img.shields.io/npm/dt/@needle-tools/engine.svg?style=flat&colorA=000000&colorB=000000"})])],-1),U=e("p",null,[e("a",{href:"https://discord.needle.tools"},[e("img",{src:"https://img.shields.io/discord/717429793926283276?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=ffffff"})])],-1),V=e("p",null,null,-1);function C(D,S){const a=i("quoteslides"),n=i("action"),l=i("actiongroup"),r=i("copyright");return c(),h("div",null,[_,o(a,null,{default:s(()=>[f,w,b,y,v,x,N,k]),_:1}),o(l,null,{default:s(()=>[o(n,{href:"getting-started"},{default:s(()=>[t(" Get started โญ ")]),_:1}),o(n,{href:"features-overview"},{default:s(()=>[t(" Features ๐จ ")]),_:1}),o(n,{href:"https://engine.needle.tools/samples"},{default:s(()=>[t(" Samples ๐ ")]),_:1}),o(n,{href:"https://forum.needle.tools"},{default:s(()=>[t(" Forum ๐ฌ ")]),_:1})]),_:1}),u(' '),B,T,o(l,null,{default:s(()=>[W,R,U]),_:1}),V,o(r)])}const I=d(m,[["render",C],["__file","index.html.vue"]]);export{I as default};
diff --git a/assets/index.html-30b5db41.js b/assets/index.html-30b5db41.js
new file mode 100644
index 000000000..9c9dc8ceb
--- /dev/null
+++ b/assets/index.html-30b5db41.js
@@ -0,0 +1,33 @@
+import{_ as a,M as e,p as t,q as o,N as p,R as n,a1 as c}from"./framework-c782e227.js";const i={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=c(`
Add this class to your project to always open with Chrome instead of your default browser (Firefox in my case) when you click "Play" or "Start Server". Note: This is an editor class and should either be put into an editor-only assembly or wrapped in #if UNITY_EDITOR and #endif.
`,2);function r(d,k){const s=e("contribution-header");return t(),o("div",null,[l,p(s,{url:"https://github.com/krisrok",author:"krisrok",page:"/docs/community/contributions/krisrok",profileImage:"https://avatars.githubusercontent.com/u/3404365?s=100&u=7025bf7e83b4a3cd72dc2cae9cec729080ee8970&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/178",title:"Always open in specific browser",gradient:"True"}),u])}const m=a(i,[["render",r],["__file","index.html.vue"]]);export{m as default};
diff --git a/assets/index.html-31811a83.js b/assets/index.html-31811a83.js
new file mode 100644
index 000000000..03ba1b009
--- /dev/null
+++ b/assets/index.html-31811a83.js
@@ -0,0 +1,38 @@
+import{_ as l,M as a,p,q as i,N as t,V as e,R as n,t as s}from"./framework-c782e227.js";const u={},r=n("p",null,"https://github.com/needle-tools/needle-engine-support/assets/30328735/92404e43-9d45-4ee2-a018-fb00412b6bd5",-1),k=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Behaviour"),n("span",{class:"token punctuation"},","),s(" serializable"),n("span",{class:"token punctuation"},","),s(" setWorldPosition "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Object3D"),n("span",{class:"token punctuation"},","),s(" Vector3 "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"three"'),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token keyword"},"const"),s(" vector1 "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Vector3"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"const"),s(" vector2 "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Vector3"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"PointerFollower"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),s("Object3D"),n("span",{class:"token punctuation"},")"),s(`
+ target`),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" Object3D"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ offset`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token builtin"},"number"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"10"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token function"},"update"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token keyword"},"void"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" cam "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("mainCamera"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" input "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("input"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token operator"},"!"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("target "),n("span",{class:"token operator"},"||"),s(),n("span",{class:"token operator"},"!"),s("cam"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token keyword"},"return"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// get relative mouse position, in range -1 to 1"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" mouse "),n("span",{class:"token operator"},"="),s(" input"),n("span",{class:"token punctuation"},"."),s("mousePositionRC"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// get world position of mouse on the near plane"),s(`
+ vector1`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"set"),n("span",{class:"token punctuation"},"("),s("mouse"),n("span",{class:"token punctuation"},"."),s("x"),n("span",{class:"token punctuation"},","),s(" mouse"),n("span",{class:"token punctuation"},"."),s("y"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token operator"},"-"),n("span",{class:"token number"},"1"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"unproject"),n("span",{class:"token punctuation"},"("),s("cam"),n("span",{class:"token operator"},"!"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// caulculate direction from camera to world mouse"),s(`
+ vector2`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"copy"),n("span",{class:"token punctuation"},"("),s("vector1"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"sub"),n("span",{class:"token punctuation"},"("),s("cam"),n("span",{class:"token punctuation"},"."),s("position"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"normalize"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// offset it to the wanted distance"),s(`
+ vector1`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"addScaledVector"),n("span",{class:"token punctuation"},"("),s("vector2"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("offset"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// apply the result"),s(`
+ `),n("span",{class:"token function"},"setWorldPosition"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("target"),n("span",{class:"token punctuation"},","),s(" vector1"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1);function d(m,b){const o=a("contribution-preview"),c=a("contributions-author");return p(),i("div",null,[t(c,{overviewLink:"/docs/community/contributions",name:"kipash",url:"https://github.com/kipash",profileImage:"https://avatars.githubusercontent.com/u/30328735?s=100&u=f28398f4575da1835d1c710d14763c69418cd0fa&v=4",githubUrl:"https://github.com/kipash"},{default:e(()=>[t(o,{title:"Calculate pointer world position",pageUrl:"/docs/community/contributions/kipash/calculate-pointer-world-position"},{default:e(()=>[r,k]),_:1})]),_:1})])}const f=l(u,[["render",d],["__file","index.html.vue"]]);export{f as default};
diff --git a/assets/index.html-375c8ddd.js b/assets/index.html-375c8ddd.js
new file mode 100644
index 000000000..aa6f6c9fc
--- /dev/null
+++ b/assets/index.html-375c8ddd.js
@@ -0,0 +1,33 @@
+import{_ as i,M as a,p as l,q as p,N as e,V as t,R as n,t as s}from"./framework-c782e227.js";const u={},r=n("p",null,[s('Add this class to your project to always open with Chrome instead of your default browser (Firefox in my case) when you click "Play" or "Start Server". Note: This is an editor class and should either be put into an editor-only assembly or wrapped in '),n("code",null,"#if UNITY_EDITOR"),s(" and "),n("code",null,"#endif"),s(".")],-1),k=n("div",{class:"language-csharp line-numbers-mode","data-ext":"cs"},[n("pre",{class:"language-csharp"},[n("code",null,[n("span",{class:"token keyword"},"using"),s(),n("span",{class:"token namespace"},[s("System"),n("span",{class:"token punctuation"},"."),s("Diagnostics")]),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"using"),s(),n("span",{class:"token namespace"},"UnityEditor"),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"using"),s(),n("span",{class:"token namespace"},"UnityEngine"),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"using"),s(),n("span",{class:"token namespace"},[s("Needle"),n("span",{class:"token punctuation"},"."),s("Engine")]),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token punctuation"},"["),n("span",{class:"token attribute"},[n("span",{class:"token class-name"},"InitializeOnLoad")]),n("span",{class:"token punctuation"},"]"),s(`
+`),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token keyword"},"static"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"CustomBrowserOpen"),s(`
+`),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"static"),s(),n("span",{class:"token function"},"CustomBrowserOpen"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token function"},"Init"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token punctuation"},"["),n("span",{class:"token attribute"},[n("span",{class:"token class-name"},"RuntimeInitializeOnLoadMethod")]),n("span",{class:"token punctuation"},"]"),s(`
+ `),n("span",{class:"token keyword"},"static"),s(),n("span",{class:"token return-type class-name"},[n("span",{class:"token keyword"},"void")]),s(),n("span",{class:"token function"},"Init"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ ActionsBrowser`),n("span",{class:"token punctuation"},"."),s("BeforeOpen "),n("span",{class:"token operator"},"+="),s(" ActionsBrowser_BeforeOpen"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(),n("span",{class:"token keyword"},"static"),s(),n("span",{class:"token return-type class-name"},[n("span",{class:"token keyword"},"void")]),s(),n("span",{class:"token function"},"ActionsBrowser_BeforeOpen"),n("span",{class:"token punctuation"},"("),n("span",{class:"token class-name"},[s("ActionsBrowser"),n("span",{class:"token punctuation"},"."),s("OpenBrowserArguments")]),s(" args"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ args`),n("span",{class:"token punctuation"},"."),s("PreventDefault "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token class-name"},[n("span",{class:"token keyword"},"string")]),s(" processArgs "),n("span",{class:"token operator"},"="),s(" args"),n("span",{class:"token punctuation"},"."),s("Url"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token class-name"},[n("span",{class:"token keyword"},"var")]),s(" psi "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token constructor-invocation class-name"},"ProcessStartInfo"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ FileName `),n("span",{class:"token operator"},"="),s(),n("span",{class:"token string"},'"chrome.exe"'),n("span",{class:"token punctuation"},","),s(`
+ Arguments `),n("span",{class:"token operator"},"="),s(` processArgs
+ `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},";"),s(`
+ Process`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"Start"),n("span",{class:"token punctuation"},"("),s("psi"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1);function d(m,b){const o=a("contribution-preview"),c=a("contributions-author");return l(),p("div",null,[e(c,{overviewLink:"/docs/community/contributions",name:"krisrok",url:"https://github.com/krisrok",profileImage:"https://avatars.githubusercontent.com/u/3404365?s=100&u=7025bf7e83b4a3cd72dc2cae9cec729080ee8970&v=4",githubUrl:"https://github.com/krisrok"},{default:t(()=>[e(o,{title:"Always open in specific browser",pageUrl:"/docs/community/contributions/krisrok/always-open-in-specific-browser"},{default:t(()=>[r,k]),_:1})]),_:1})])}const w=i(u,[["render",d],["__file","index.html.vue"]]);export{w as default};
diff --git a/assets/index.html-393d50c1.js b/assets/index.html-393d50c1.js
new file mode 100644
index 000000000..cac37f457
--- /dev/null
+++ b/assets/index.html-393d50c1.js
@@ -0,0 +1 @@
+import{_ as i,M as t,p as s,q as c,N as o,V as n,R as e}from"./framework-c782e227.js";const l={},u=e("p",null,"This is live for preview over at https://needle-ar.glitch.me/",-1),h=e("p",null,"I will share a github repository if others are interested in collaborating on this, so far I have just sorted functionality for spawning a product model and a floor plane that is large to move it around on. Scale and Rotate work with two finger touches.",-1),d=e("p",null,"I am hoping to figure out how to show and raycast against AR detected planes over here which will remove the need for a large floor plane to raycast against โ Unknown",-1),p=e("p",null,"Public Github Repository: https://github.com/ROBYER1/Needle-AR-Demo",-1);function _(m,f){const a=t("contribution-preview"),r=t("contributions-author");return s(),c("div",null,[o(r,{overviewLink:"/docs/community/contributions",name:"ROBYER1",url:"https://github.com/ROBYER1",profileImage:"https://avatars.githubusercontent.com/u/10745594?s=100&u=daf2c8b5dad729e556ae2a01c721672b24bc108a&v=4",githubUrl:"https://github.com/ROBYER1"},{default:n(()=>[o(a,{title:"AR Move/Scale/Rotate Controls for Needle on Mobile",pageUrl:"/docs/community/contributions/robyer1/ar-move-scale-rotate-controls-for-needle-on-mobile"},{default:n(()=>[u,h,d,p]),_:1})]),_:1})])}const g=i(l,[["render",_],["__file","index.html.vue"]]);export{g as default};
diff --git a/assets/index.html-3b7863bb.js b/assets/index.html-3b7863bb.js
new file mode 100644
index 000000000..df026414d
--- /dev/null
+++ b/assets/index.html-3b7863bb.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-8daa1a0e","path":"/","title":"","lang":"en-US","frontmatter":{"next":"getting-started.md","sidebar":false,"editLink":false,"lastUpdated":false,"footer":"Copyright ยฉ 2022 Needle Tools GmbH","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/needle engine.png"}],["meta",{"name":"og:description","content":"---\\nNeedle Engine is a web engine for complex and simple 3D applications alike. Run on your machine and deploy anywhere. It is flexible, extensible and has built-in support for collaboration and XR! It is built around the glTF standard for 3D assets. Powerful integrations for Unity and Blender allow artists and developers to collaborate and manage web applications inside battle-tested 3d editors. Needle Engine Integrations allow you to use Editor features for exporting models, author materials, animate and sequence animations, bake lightmaps and more with ease. Our powerful compression and optimization pipeline for the web make sure your files are ready, small and load fast!"}]],"description":"---\\nNeedle Engine is a web engine for complex and simple 3D applications alike. Run on your machine and deploy anywhere. It is flexible, extensible and has built-in support for collaboration and XR! It is built around the glTF standard for 3D assets. Powerful integrations for Unity and Blender allow artists and developers to collaborate and manage web applications inside battle-tested 3d editors. Needle Engine Integrations allow you to use Editor features for exporting models, author materials, animate and sequence animations, bake lightmaps and more with ease. Our powerful compression and optimization pipeline for the web make sure your files are ready, small and load fast!"},"headers":[],"git":{},"filePathRelative":"index.md"}');export{e as data};
diff --git a/assets/index.html-3fc0407e.js b/assets/index.html-3fc0407e.js
new file mode 100644
index 000000000..3c151a87b
--- /dev/null
+++ b/assets/index.html-3fc0407e.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-93aa7836","path":"/community/contributions/marwie/everywhere-action-emphasize-on-click/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/marwie: everywhere action emphasize on click.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-441fa19f.js b/assets/index.html-441fa19f.js
new file mode 100644
index 000000000..d8f9ba817
--- /dev/null
+++ b/assets/index.html-441fa19f.js
@@ -0,0 +1 @@
+import{_ as o,M as n,p as a,q as r,N as i,R as e}from"./framework-c782e227.js";const s={},l=e("p",null,[e("a",{href:"/docs/community/contributions"},"Overview")],-1),c=e("p",null,"This is live for preview over at https://needle-ar.glitch.me/",-1),h=e("p",null,"I will share a github repository if others are interested in collaborating on this, so far I have just sorted functionality for spawning a product model and a floor plane that is large to move it around on. Scale and Rotate work with two finger touches.",-1),u=e("p",null,"I am hoping to figure out how to show and raycast against AR detected planes over here which will remove the need for a large floor plane to raycast against โ Unknown",-1),d=e("p",null,"Public Github Repository: https://github.com/ROBYER1/Needle-AR-Demo",-1);function p(_,m){const t=n("contribution-header");return a(),r("div",null,[l,i(t,{url:"https://github.com/ROBYER1",author:"ROBYER1",page:"/docs/community/contributions/robyer1",profileImage:"https://avatars.githubusercontent.com/u/10745594?s=100&u=daf2c8b5dad729e556ae2a01c721672b24bc108a&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/174",title:"AR Move/Scale/Rotate Controls for Needle on Mobile",gradient:"True"}),c,h,u,d])}const g=o(s,[["render",p],["__file","index.html.vue"]]);export{g as default};
diff --git a/assets/index.html-45b10041.js b/assets/index.html-45b10041.js
new file mode 100644
index 000000000..59e34aecd
--- /dev/null
+++ b/assets/index.html-45b10041.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-3721011d","path":"/community/contributions/web3kev/vertical-move-in-vr-using-the-right-joystick-quest/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/web3kev: vertical move in vr using the right joystick quest.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-45d8bb7e.js b/assets/index.html-45d8bb7e.js
new file mode 100644
index 000000000..ea23783d6
--- /dev/null
+++ b/assets/index.html-45d8bb7e.js
@@ -0,0 +1,23 @@
+import{_ as a,M as t,p as e,q as p,N as o,R as n,a1 as c}from"./framework-c782e227.js";const i={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=c(`
This is an example from our Everywhere Actions. The following script hides an object on start on Android and on iOS AR
`,2);function r(d,k){const s=t("contribution-header");return e(),p("div",null,[l,o(s,{url:"https://github.com/marwie",author:"marwie",page:"/docs/community/contributions/marwie",profileImage:"https://avatars.githubusercontent.com/u/5083203?s=100&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/156",title:"USDZ: Hide Object on Start",gradient:"True"}),u])}const v=a(i,[["render",r],["__file","index.html.vue"]]);export{v as default};
diff --git a/assets/index.html-5649cadf.js b/assets/index.html-5649cadf.js
new file mode 100644
index 000000000..fa8d2bd01
--- /dev/null
+++ b/assets/index.html-5649cadf.js
@@ -0,0 +1,30 @@
+import{_ as p,M as s,p as c,q as i,N as a,R as n,t as e,a1 as l}from"./framework-c782e227.js";const u={},r=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),d={href:"https://linen-upbeat-sailboat.glitch.me/",target:"_blank",rel:"noopener noreferrer"},k=l(`
import{ Behaviour, ClearFlags, RGBAColor }from"@needle-tools/engine";
+
+exportclassVideoBackgroundextendsBehaviour{
+
+ asyncawake(){
+ // create video element and put it inside the <needle-engine> component
+ const video = document.createElement("video");
+ video.style.cssText =\`
+ position: fixed;
+ min-width: 100%;
+ min-height: 100%;
+ z-index: -1;
+ \`
+ this.context.domElement.shadowRoot!.appendChild(video);
+
+ // get webcam input
+ const input =await navigator.mediaDevices.getUserMedia({ video:true})
+ if(!input)return;
+ video.srcObject = input;
+ video.play();
+
+ // make sure the camera background is transparent
+ const camera =this.context.mainCameraComponent;
+ if(camera){
+ camera.clearFlags = ClearFlags.SolidColor;
+ camera.backgroundColor =newRGBAColor(125,125,125,0);
+ }
+ }
+}
+
`,1);function m(v,b){const t=s("contribution-header"),o=s("ExternalLinkIcon");return c(),i("div",null,[r,a(t,{url:"https://github.com/marwie",author:"marwie",page:"/docs/community/contributions/marwie",profileImage:"https://avatars.githubusercontent.com/u/5083203?s=100&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/166",title:"Camera Video Background",gradient:"True"}),n("p",null,[e("Put it anywhere in your scene to render a camera video behind your 3D scene "),n("a",d,[e("Live demo"),a(o)])]),k])}const h=p(u,[["render",m],["__file","index.html.vue"]]);export{h as default};
diff --git a/assets/index.html-56d5893e.js b/assets/index.html-56d5893e.js
new file mode 100644
index 000000000..dc95c11ff
--- /dev/null
+++ b/assets/index.html-56d5893e.js
@@ -0,0 +1 @@
+import{_ as l,M as r,p as u,q as m,R as n,t as c,N as t,V as o}from"./framework-c782e227.js";const b={},d=n("h1",null,"Community Scripts",-1),p={href:"https://github.com/needle-tools/needle-engine-support/discussions/categories/share",target:"_blank",rel:"noopener noreferrer"};function h(f,g){const a=r("ExternalLinkIcon"),e=r("contribution-listentry"),i=r("contribution-header"),s=r("contributions-overview");return u(),m("div",null,[d,n("p",null,[c("To contribute a script, please create a new discussion in the "),n("a",p,[c("Share category"),t(a)])]),t(s,null,{default:o(()=>[t(i,{url:"https://github.com/llllkatjallll",author:"llllkatjallll",page:"/docs/community/contributions/llllkatjallll",profileImage:"https://avatars.githubusercontent.com/u/38395689?s=100&u=7ce0fef973c4819c4f07823568d6f6061abfe410&v=4"},{default:o(()=>[t(e,{title:"Set fallback material for USDZ exporter",url:"/docs/community/contributions/llllkatjallll/set-fallback-material-for-usdz-exporter"})]),_:1}),t(i,{url:"https://github.com/krisrok",author:"krisrok",page:"/docs/community/contributions/krisrok",profileImage:"https://avatars.githubusercontent.com/u/3404365?s=100&u=7025bf7e83b4a3cd72dc2cae9cec729080ee8970&v=4"},{default:o(()=>[t(e,{title:"Always open in specific browser",url:"/docs/community/contributions/krisrok/always-open-in-specific-browser"})]),_:1}),t(i,{url:"https://github.com/ROBYER1",author:"ROBYER1",page:"/docs/community/contributions/robyer1",profileImage:"https://avatars.githubusercontent.com/u/10745594?s=100&u=daf2c8b5dad729e556ae2a01c721672b24bc108a&v=4"},{default:o(()=>[t(e,{title:"AR Move/Scale/Rotate Controls for Needle on Mobile",url:"/docs/community/contributions/robyer1/ar-move-scale-rotate-controls-for-needle-on-mobile"})]),_:1}),t(i,{url:"https://github.com/marwie",author:"marwie",page:"/docs/community/contributions/marwie",profileImage:"https://avatars.githubusercontent.com/u/5083203?s=100&v=4"},{default:o(()=>[t(e,{title:"Camera Video Background",url:"/docs/community/contributions/marwie/camera-video-background"}),t(e,{title:"USDZ: Hide Object on Start",url:"/docs/community/contributions/marwie/usdz-hide-object-on-start"}),t(e,{title:"Everywhere Action: Emphasize on Click",url:"/docs/community/contributions/marwie/everywhere-action-emphasize-on-click"}),t(e,{title:"Control a Timeline by scroll",url:"/docs/community/contributions/marwie/control-a-timeline-by-scroll"}),t(e,{title:"Code Contribution Example",url:"/docs/community/contributions/marwie/code-contribution-example"})]),_:1}),t(i,{url:"https://github.com/kipash",author:"kipash",page:"/docs/community/contributions/kipash",profileImage:"https://avatars.githubusercontent.com/u/30328735?s=100&u=f28398f4575da1835d1c710d14763c69418cd0fa&v=4"},{default:o(()=>[t(e,{title:"Calculate pointer world position",url:"/docs/community/contributions/kipash/calculate-pointer-world-position"})]),_:1}),t(i,{url:"https://github.com/Web3Kev",author:"Web3Kev",page:"/docs/community/contributions/web3kev",profileImage:"https://avatars.githubusercontent.com/u/106066970?s=100&v=4"},{default:o(()=>[t(e,{title:"Vertical Move in VR using the right joystick (Quest)",url:"/docs/community/contributions/web3kev/vertical-move-in-vr-using-the-right-joystick-quest"}),t(e,{title:"Squeeze to Scale (Object or World) in VR",url:"/docs/community/contributions/web3kev/squeeze-to-scale-object-or-world-in-vr"}),t(e,{title:"Network instantiation of multiple objects",url:"/docs/community/contributions/web3kev/network-instantiation-of-multiple-objects"})]),_:1})]),_:1})])}const v=l(b,[["render",h],["__file","index.html.vue"]]);export{v as default};
diff --git a/assets/index.html-5d6886aa.js b/assets/index.html-5d6886aa.js
new file mode 100644
index 000000000..1ce32e982
--- /dev/null
+++ b/assets/index.html-5d6886aa.js
@@ -0,0 +1,150 @@
+import{_ as p,M as o,p as i,q as u,N as a,V as t,R as n,t as s}from"./framework-c782e227.js";const r={},k={href:"https://linen-upbeat-sailboat.glitch.me/",target:"_blank",rel:"noopener noreferrer"},d=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Behaviour"),n("span",{class:"token punctuation"},","),s(" ClearFlags"),n("span",{class:"token punctuation"},","),s(" RGBAColor "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"VideoBackground"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token keyword"},"async"),s(),n("span",{class:"token function"},"awake"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"// create video element and put it inside the component"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" video "),n("span",{class:"token operator"},"="),s(" document"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"createElement"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"video"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ video`),n("span",{class:"token punctuation"},"."),s("style"),n("span",{class:"token punctuation"},"."),s("cssText "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token template-string"},[n("span",{class:"token template-punctuation string"},"`"),n("span",{class:"token string"},`
+ position: fixed;
+ min-width: 100%;
+ min-height: 100%;
+ z-index: -1;
+ `),n("span",{class:"token template-punctuation string"},"`")]),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("domElement"),n("span",{class:"token punctuation"},"."),s("shadowRoot"),n("span",{class:"token operator"},"!"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"appendChild"),n("span",{class:"token punctuation"},"("),s("video"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// get webcam input"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" input "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"await"),s(" navigator"),n("span",{class:"token punctuation"},"."),s("mediaDevices"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"getUserMedia"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},"{"),s(" video"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token boolean"},"true"),s(),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token operator"},"!"),s("input"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token keyword"},"return"),n("span",{class:"token punctuation"},";"),s(`
+ video`),n("span",{class:"token punctuation"},"."),s("srcObject "),n("span",{class:"token operator"},"="),s(" input"),n("span",{class:"token punctuation"},";"),s(`
+ video`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"play"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// make sure the camera background is transparent"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" camera "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("mainCameraComponent"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),s("camera"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ camera`),n("span",{class:"token punctuation"},"."),s("clearFlags "),n("span",{class:"token operator"},"="),s(" ClearFlags"),n("span",{class:"token punctuation"},"."),s("SolidColor"),n("span",{class:"token punctuation"},";"),s(`
+ camera`),n("span",{class:"token punctuation"},"."),s("backgroundColor "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"RGBAColor"),n("span",{class:"token punctuation"},"("),n("span",{class:"token number"},"125"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token number"},"125"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token number"},"125"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),m=n("p",null,[s("This is an example from our "),n("em",null,"Everywhere Actions"),s(". The following script hides an object on start on Android and on iOS AR")],-1),b=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"HideOnStart"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(),n("span",{class:"token keyword"},"implements"),s(),n("span",{class:"token class-name"},"UsdzBehaviour"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token function"},"start"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},"."),s("visible "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"createBehaviours"),n("span",{class:"token punctuation"},"("),s("ext"),n("span",{class:"token punctuation"},","),s(" model"),n("span",{class:"token punctuation"},","),s(" _context"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),s("model"),n("span",{class:"token punctuation"},"."),s("uuid "),n("span",{class:"token operator"},"==="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},"."),s("uuid"),n("span",{class:"token punctuation"},")"),s(`
+ ext`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"addBehavior"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"BehaviorModel"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"HideOnStart_"'),s(),n("span",{class:"token operator"},"+"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},"."),s("name"),n("span",{class:"token punctuation"},","),s(`
+ TriggerBuilder`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"sceneStartTrigger"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},","),s(`
+ ActionBuilder`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"fadeAction"),n("span",{class:"token punctuation"},"("),s("model"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"beforeCreateDocument"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},"."),s("visible "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"afterCreateDocument"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},"."),s("visible "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),v=n("p",null,"Example for adding custom USDZ behaviours for iOS AR",-1),h=n("p",null,"This is an USDZ / iOS AR only example",-1),y=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"EmphasizeOnClick"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(),n("span",{class:"token keyword"},"implements"),s(),n("span",{class:"token class-name"},"UsdzBehaviour"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ target`),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" Object3D"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ duration`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token builtin"},"number"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"0.5"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ motionType`),n("span",{class:"token operator"},":"),s(" MotionType "),n("span",{class:"token operator"},"="),s(" MotionType"),n("span",{class:"token punctuation"},"."),s("bounce"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token function"},"beforeCreateDocument"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"createBehaviours"),n("span",{class:"token punctuation"},"("),s("ext"),n("span",{class:"token punctuation"},","),s(" model"),n("span",{class:"token punctuation"},","),s(" _context"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token operator"},"!"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("target"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token keyword"},"return"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),s("model"),n("span",{class:"token punctuation"},"."),s("uuid "),n("span",{class:"token operator"},"==="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},"."),s("uuid"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" emphasize "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"BehaviorModel"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"emphasize "'),s(),n("span",{class:"token operator"},"+"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("name"),n("span",{class:"token punctuation"},","),s(`
+ TriggerBuilder`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"tapTrigger"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},","),s(`
+ ActionBuilder`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"emphasize"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("target"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("duration"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("motionType"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"undefined"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token string"},'"basic"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},","),s(`
+ `),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ ext`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"addBehavior"),n("span",{class:"token punctuation"},"("),s("emphasize"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"afterCreateDocument"),n("span",{class:"token punctuation"},"("),s("_ext"),n("span",{class:"token punctuation"},","),s(" _context"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),w=n("p",null,"Use the mouse wheel or touch delta to update a timeline's time.",-1),g=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Behaviour"),n("span",{class:"token punctuation"},","),s(" PlayableDirector"),n("span",{class:"token punctuation"},","),s(" serializeable "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Mathf "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token comment"},"// Example of setting a timeline's time "),s(`
+`),n("span",{class:"token comment"},"// without relying on any HTML elements."),s(`
+`),n("span",{class:"token comment"},"// Here we directly use the mousewheel scroll and the touch delta"),s(`
+
+`),n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"ScrollTimeline_2"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializeable")]),n("span",{class:"token punctuation"},"("),s("PlayableDirector"),n("span",{class:"token punctuation"},")"),s(`
+ timeline`),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" PlayableDirector"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializeable")]),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ scrollSpeed`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token builtin"},"number"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"0.5"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializeable")]),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ lerpSpeed`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token builtin"},"number"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"2.5"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(" targetTime"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token builtin"},"number"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token function"},"start"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("timeline"),n("span",{class:"token operator"},"?."),n("span",{class:"token function"},"pause"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// Grab the mousewheel event"),s(`
+ window`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"addEventListener"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"wheel"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token punctuation"},"("),s("evt"),n("span",{class:"token operator"},":"),s(" WheelEvent"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"=>"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"updateTime"),n("span",{class:"token punctuation"},"("),s("evt"),n("span",{class:"token punctuation"},"."),s("deltaY"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// Touch events are a bit more complicated"),s(`
+ `),n("span",{class:"token comment"},"// We need to keep track of the last touch position"),s(`
+ `),n("span",{class:"token comment"},"// and calculate the delta between the current and the last position"),s(`
+ `),n("span",{class:"token keyword"},"let"),s(" lastTouchPosition "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token operator"},"-"),n("span",{class:"token number"},"1"),n("span",{class:"token punctuation"},";"),s(`
+ window`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"addEventListener"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"touchmove"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token punctuation"},"("),s("evt"),n("span",{class:"token operator"},":"),s(" TouchEvent"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"=>"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" delta "),n("span",{class:"token operator"},"="),s(" evt"),n("span",{class:"token punctuation"},"."),s("touches"),n("span",{class:"token punctuation"},"["),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},"."),s("clientY "),n("span",{class:"token operator"},"-"),s(" lastTouchPosition"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token comment"},"// We only want to apply the delta if it's not TOO big"),s(`
+ `),n("span",{class:"token comment"},"// e.g. when the user is scrolling the page"),s(`
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),s("delta "),n("span",{class:"token operator"},"<"),s(),n("span",{class:"token number"},"10"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"updateTime"),n("span",{class:"token punctuation"},"("),n("span",{class:"token operator"},"-"),s("delta"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token comment"},"// Update the last touch position"),s(`
+ lastTouchPosition `),n("span",{class:"token operator"},"="),s(" evt"),n("span",{class:"token punctuation"},"."),s("touches"),n("span",{class:"token punctuation"},"["),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},"."),s("clientY"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(),n("span",{class:"token function"},"updateTime"),n("span",{class:"token punctuation"},"("),s("delta"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token operator"},"!"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("timeline"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token keyword"},"return"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("targetTime "),n("span",{class:"token operator"},"+="),s(" delta "),n("span",{class:"token operator"},"*"),s(),n("span",{class:"token number"},"0.01"),s(),n("span",{class:"token operator"},"*"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("scrollSpeed"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("targetTime "),n("span",{class:"token operator"},"="),s(" Mathf"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"clamp"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("targetTime"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("timeline"),n("span",{class:"token punctuation"},"."),s("duration"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"onBeforeRender"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token keyword"},"void"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token operator"},"!"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("timeline"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token keyword"},"return"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("timeline"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"pause"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("timeline"),n("span",{class:"token punctuation"},"."),s("time "),n("span",{class:"token operator"},"="),s(" Mathf"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"lerp"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("timeline"),n("span",{class:"token punctuation"},"."),s("time"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("targetTime"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("lerpSpeed "),n("span",{class:"token operator"},"*"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("time"),n("span",{class:"token punctuation"},"."),s("deltaTime"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("timeline"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"evaluate"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),f=n("p",null,"This is mostly a basic example on how to contribute. It will then be added on our documentation contributions page: https://engine.needle.tools/docs/community/contributions",-1),_=n("p",null,"Please include at least one code snippet, for example like this:",-1),x=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Behaviour"),n("span",{class:"token punctuation"},","),s(" serializable "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Object3D "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"three"'),s(`
+
+`),n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"MyComponent"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),s("Object3D"),n("span",{class:"token punctuation"},")"),s(`
+ myObjectReference`),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" Object3D"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token function"},"start"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"Hello world"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"update"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"// called every frame"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1);function T(B,C){const c=o("ExternalLinkIcon"),e=o("contribution-preview"),l=o("contributions-author");return i(),u("div",null,[a(l,{overviewLink:"/docs/community/contributions",name:"marwie",url:"https://github.com/marwie",profileImage:"https://avatars.githubusercontent.com/u/5083203?s=100&v=4",githubUrl:"https://github.com/marwie"},{default:t(()=>[a(e,{title:"Camera Video Background",pageUrl:"/docs/community/contributions/marwie/camera-video-background"},{default:t(()=>[n("p",null,[s("Put it anywhere in your scene to render a camera video behind your 3D scene "),n("a",k,[s("Live demo"),a(c)])]),d]),_:1}),a(e,{title:"USDZ: Hide Object on Start",pageUrl:"/docs/community/contributions/marwie/usdz-hide-object-on-start"},{default:t(()=>[m,b]),_:1}),a(e,{title:"Everywhere Action: Emphasize on Click",pageUrl:"/docs/community/contributions/marwie/everywhere-action-emphasize-on-click"},{default:t(()=>[v,h,y]),_:1}),a(e,{title:"Control a Timeline by scroll",pageUrl:"/docs/community/contributions/marwie/control-a-timeline-by-scroll"},{default:t(()=>[w,g]),_:1}),a(e,{title:"Code Contribution Example",pageUrl:"/docs/community/contributions/marwie/code-contribution-example"},{default:t(()=>[f,_,x]),_:1})]),_:1})])}const z=p(r,[["render",T],["__file","index.html.vue"]]);export{z as default};
diff --git a/assets/index.html-615ba1f2.js b/assets/index.html-615ba1f2.js
new file mode 100644
index 000000000..0c0279a7e
--- /dev/null
+++ b/assets/index.html-615ba1f2.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-e6fbcab8","path":"/community/contributions/llllkatjallll/set-fallback-material-for-usdz-exporter/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/llllkatjallll: set fallback material for usdz exporter.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-6454ce7b.js b/assets/index.html-6454ce7b.js
new file mode 100644
index 000000000..2518c051a
--- /dev/null
+++ b/assets/index.html-6454ce7b.js
@@ -0,0 +1,62 @@
+import{_ as a,M as t,p,q as e,N as o,R as n,a1 as c}from"./framework-c782e227.js";const i={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=c(`
The following code will enable Quest users (haven't tested with other devices) to move up and down with the right-joystick\`s y axis. (the x axis being used for snap-turns).
This code will interfere with the teleport script when accidentally pointing towards an object and trying to move up. It is recommended to remove the teleport script for that matter.
`,4);function r(k,d){const s=t("contribution-header");return p(),e("div",null,[l,o(s,{url:"https://github.com/Web3Kev",author:"Web3Kev",page:"/docs/community/contributions/web3kev",profileImage:"https://avatars.githubusercontent.com/u/106066970?s=100&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/158",title:"Vertical Move in VR using the right joystick (Quest)",gradient:"True"}),u])}const m=a(i,[["render",r],["__file","index.html.vue"]]);export{m as default};
diff --git a/assets/index.html-67a46bcb.js b/assets/index.html-67a46bcb.js
new file mode 100644
index 000000000..ed6701ad5
--- /dev/null
+++ b/assets/index.html-67a46bcb.js
@@ -0,0 +1,38 @@
+import{_ as a,M as t,p as e,q as o,N as p,R as n,a1 as c}from"./framework-c782e227.js";const i={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=c(`
import{ Behaviour, serializable, setWorldPosition }from"@needle-tools/engine";
+import{ Object3D, Vector3 }from"three";
+
+const vector1 =newVector3();
+const vector2 =newVector3();
+
+exportclassPointerFollowerextendsBehaviour{
+
+ @serializable(Object3D)
+ target?: Object3D;
+
+ @serializable()
+ offset:number=10;
+
+ update():void{
+ const cam =this.context.mainCamera;
+ const input =this.context.input;
+
+ if(!this.target ||!cam)
+ return;
+
+ // get relative mouse position, in range -1 to 1
+ const mouse = input.mousePositionRC;
+
+ // get world position of mouse on the near plane
+ vector1.set(mouse.x, mouse.y,-1).unproject(cam!);
+
+ // caulculate direction from camera to world mouse
+ vector2.copy(vector1).sub(cam.position).normalize();
+
+ // offset it to the wanted distance
+ vector1.addScaledVector(vector2,this.offset);
+
+ // apply the result
+ setWorldPosition(this.target, vector1);
+ }
+}
+
`,2);function r(k,d){const s=t("contribution-header");return e(),o("div",null,[l,p(s,{url:"https://github.com/kipash",author:"kipash",page:"/docs/community/contributions/kipash",profileImage:"https://avatars.githubusercontent.com/u/30328735?s=100&u=f28398f4575da1835d1c710d14763c69418cd0fa&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/165",title:"Calculate pointer world position",gradient:"True"}),u])}const m=a(i,[["render",r],["__file","index.html.vue"]]);export{m as default};
diff --git a/assets/index.html-6af3f72b.js b/assets/index.html-6af3f72b.js
new file mode 100644
index 000000000..a569acdfe
--- /dev/null
+++ b/assets/index.html-6af3f72b.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-e9b14526","path":"/community/contributions/marwie/control-a-timeline-by-scroll/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/marwie: control a timeline by scroll.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-6bc0a449.js b/assets/index.html-6bc0a449.js
new file mode 100644
index 000000000..f5d268441
--- /dev/null
+++ b/assets/index.html-6bc0a449.js
@@ -0,0 +1,36 @@
+import{_ as d}from"./logo-b695892f.js";import{_ as u}from"./texture-compression-8cd31165.js";import{_ as h,M as r,p as m,q as b,N as t,R as e,V as a,t as n,a1 as i}from"./framework-c782e227.js";const k="/docs/blender/settings.webp",g="/docs/blender/project-panel.webp",f="/docs/blender/project-panel-2.webp",w="/docs/blender/project-panel-3.webp",_="/docs/blender/settings-color-management.webp",y="/docs/blender/dont-export.webp",v="/docs/blender/animatorcontroller-open.webp",x="/docs/blender/animatorcontroller-overview.webp",j="/docs/blender/animatorcontroller-assigning.webp",T="/docs/blender/timeline_setup.webp",C="/docs/blender/timeline.webp",P="/docs/blender/components-panel.webp",B="/docs/blender/components-panel-select.webp",A="/docs/blender/remove-component.webp",N="/docs/blender/lightmapping-object.webp",S="/docs/blender/lightmapping-scene-panel.webp",I="/docs/blender/lightmapping-panel.webp",E="/docs/blender/updates.webp",D="/docs/blender/bugreporter.webp",L={},R=e("br",null,null,-1),Y=e("img",{src:d,style:{"max-height":"70px"}},null,-1),q=e("h1",{id:"needle-engine-for-blender",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#needle-engine-for-blender","aria-hidden":"true"},"#"),n(" Needle Engine for Blender")],-1),U=e("p",null,[n("Thank you for using Needle Engine for Blender Alpha."),e("br"),n(" With this addon you can create highly interactive and optimized WebGL and WebXR experiences inside Blender that run using Needle Engine and three.js."),e("br"),n(" You'll be able to sequence animations, easily lightmap your scenes, add interactivity or create your own scripts written in Typescript or Javascript that run on the web. You own your content!")],-1),M=e("p",null,[e("em",null,"Automatically export HDRI environment lights directly from blender. Save to reload your local server")],-1),z=e("p",null,[e("em",null,[n("Create and export "),e("a",{href:"#animatorcontroller"},"animator statemachines"),n(" for controlling complex character animations")])],-1),V=e("h1",{id:"content-overview",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#content-overview","aria-hidden":"true"},"#"),n(" Content Overview")],-1),W={class:"table-of-contents"},F=e("h2",{id:"preface",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#preface","aria-hidden":"true"},"#"),n(" Preface")],-1),G=e("strong",null,"Your feedback is invaluable",-1),O=e("br",null,null,-1),H={href:"https://github.com/needle-tools/needle-engine-support/discussions",target:"_blank",rel:"noopener noreferrer"},J={href:"https://discord.needle.tools",target:"_blank",rel:"noopener noreferrer"},X={href:"https://github.com/needle-tools/needle-engine-support/issues",target:"_blank",rel:"noopener noreferrer"},$=i('
',5),K=e("strong",null,"Download Needle Engine for Blender",-1),Q=i('
The Blender addon is downloaded as a zip file. In Blender go to File / Settings / Add-ons and click the Install button. Then select the downloaded zip to install it.
First create or open a new blend file that you want to be exported to the web. Open the Properties window open the scene category. Select a Project Path in the Needle Engine panel. Then click Generate Project. It will automatically install and start the server - once it has finished your browser should open and the threejs scene will load.
By default your scene will automatically re-exported when you save the blend file. If the local server is running (e.g. by clicking Run Project) the website will automatically refresh with your changed model.
When your web project already exists and you just want to continue working on the website click the blue Run Project button to start the local server:
The path to your web project. You can use the little folder button on the right to select a different path.
The Run Project button shows up when the Project path shows to a valid web project. A web project is valid when it contains a package.json
Directory open the directory of your web project (the Project Path)
This button re-exports the current scene as a glb to your local web project. This also happens by default when saving your blend file.
Code Editor tries to open the vscode workspace in your web project
If you work with multiple scenes in one blend file you can configure which scene is your "main" scene and should be exported to the web. If any of your components references another scene they will also be exported as separate glb files.
Use the Build: Development or Build: Production buttons when you want to upload your web project to a server. This will bundle your web project and produce the files that you can upload. When clicking Build: Production it will also apply optimization to your textures (they will be compressed for the web)
By default the blender viewport is set to Filmic - with this setting your colors in Blender and in three.js will not match. To fix this go to the Blender Render category and in the ColorManagement panel select View Transform: Standard
For simple usecases you can use the Animation component for playback of one or multiple animationclips. Just select your object, add an Animation component and assign the clip (you can add additional clips to be exported to the clips array. By default it will only playback the first clip assigned when playAutomatically is enabled. You can trigger the other clips using a simple custom typescript component)
The animator controller can be created for more complex scenarios. It works as a statemachine which allows you to create multiple animation states in a graph and configure conditions and interpolation settings for transitioning between those.
The Parameters node will be created once you add a first node. Select it to setup parameters to be used in transitions (via the Node panel on the right border)
This is an AnimatorState. the orange state is the start state (it can be changed using the Set default state button in the Node/Properties panel)
The Properties for an AnimatorState can be used to setup one or multiple transitions to other states. Use the Conditions array to select parameters that must match the condition for doing the transition.
To use an AnimatorController add an Animator component to the root object of your animations and select the AnimatorController asset that you want to use for this object.
You can set the Animator parameters from typescript or by e.g. using the event of a Button component:
Exporting Blender nla tracks to threejs. Add a PlayableDirector component (via Add Component) to a any blender object. Assign the objects in the animation tracks list in the component for which you want the nla tracks to be exported.
Code example for interactive timeline playback
Add this script to src/scripts (see custom components section) and add it to any object in Blender to make a timeline's time be controlled by scrolling in the browsers
You can add or remove components to objects in your hierarchy using the Needle Components panel:
For example by adding an OrbitControls component to the camera object you get basic camera controls for mobile and desktop devicesAdjust settings for each component in their respective panels
Components can be removed using the X button in the lower right:
Custom components can also be easily added by simply writing Typescript classes. They will automatically compile and show up in Blender when saved.
',20),ne=e("code",null,".ts",-1),te=e("code",null,"src/scripts",-1),ae={href:"http://docs.needle.tools/scripting",target:"_blank",rel:"noopener noreferrer"},se=e("div",{class:"custom-container warning"},[e("p",{class:"custom-container-title"},"Note"),e("p",null,[n("Make sure "),e("code",null,"@needle-tools/needle-component-compiler"),n(" 2.x is installed in your web project (package.json devDependencies)")])],-1),oe=e("h2",{id:"lightmapping",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#lightmapping","aria-hidden":"true"},"#"),n(" Lightmapping ๐ก")],-1),ie=e("p",null,[n("Needle Lightmapping will automatically generate lightmap UVs for all models marked to be lightmapped. For lightmapping to work you need at least one light and one object with "),e("code",null,"Lightmapped"),n(" turned on.")],-1),le={class:"custom-container danger"},re=e("p",{class:"custom-container-title"},"Please keep in mind:",-1),pe={href:"https://discord.needle.tools",target:"_blank",rel:"noopener noreferrer"},ce={href:"https://engine.needle.tools/downloads/blender/lightmaps.blend",target:"_blank",rel:"noopener noreferrer"},de=i('
Use the Needle Object panel to enable lightmapping for a mesh object or light:
For quick access to lightmap settings and baking options you can use the scene view panel in the Needle tab:
Alternatively you can also use the Lightmapping panel in the Render Properties tab:
',4),be={href:"https://discord.needle.tools",target:"_blank",rel:"noopener noreferrer"},ke=e("p",null,[n("Please also check the logs in Blender. You can find logs specific to the Needle Engine Addon via "),e("code",null,"Help/Needle"),n(" in Blender.")],-1),ge=e("h3",{id:"integrated-bugreporter",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#integrated-bugreporter","aria-hidden":"true"},"#"),n(" Integrated Bugreporter")],-1),fe=e("p",null,[e("img",{src:D,alt:"Needle Blender bugreporter panel"}),e("br"),n(" You can also automatically create and upload a bugreport directly from Blender (this currently requires a node.js web project being setup). Uploaded bugreports will solely used for debugging, they are encrypted on our backend and will deleted after 30 days.")],-1);function we(_e,ye){const l=r("video-embed"),s=r("router-link"),o=r("ExternalLinkIcon"),c=r("needle-button"),p=r("RouterLink");return m(),b("div",null,[R,Y,q,U,t(l,{src:"/docs/blender/environment-light.mp4"}),M,t(l,{src:"/docs/blender/animatorcontroller-web.mp4"}),z,V,e("nav",W,[e("ul",null,[e("li",null,[t(s,{to:"#preface"},{default:a(()=>[n("Preface")]),_:1})]),e("li",null,[t(s,{to:"#download-and-installation"},{default:a(()=>[n("Download and Installation ๐ฟ")]),_:1}),e("ul",null,[e("li",null,[t(s,{to:"#step-1-install-blender-3.6-or-4.0"},{default:a(()=>[n("Step 1 โข Install Blender 3.6 or 4.0")]),_:1})]),e("li",null,[t(s,{to:"#step-2-install-node.js-optional-but-recommended"},{default:a(()=>[n("Step 2 โข Install Node.js (optional but recommended)")]),_:1})]),e("li",null,[t(s,{to:"#step-3-download-and-install-the-addon"},{default:a(()=>[n("Step 3 โข Download and Install the addon")]),_:1})])])]),e("li",null,[t(s,{to:"#getting-started"},{default:a(()=>[n("Getting Started ๐ฉ")]),_:1}),e("ul",null,[e("li",null,[t(s,{to:"#project-panel-overview"},{default:a(()=>[n("Project Panel overview")]),_:1})])])]),e("li",null,[t(s,{to:"#blender-settings"},{default:a(()=>[n("Blender Settings")]),_:1}),e("ul",null,[e("li",null,[t(s,{to:"#color-management"},{default:a(()=>[n("Color Management")]),_:1})])])]),e("li",null,[t(s,{to:"#export"},{default:a(()=>[n("Export")]),_:1})]),e("li",null,[t(s,{to:"#animation"},{default:a(()=>[n("Animation ๐")]),_:1}),e("ul",null,[e("li",null,[t(s,{to:"#animatorcontroller"},{default:a(()=>[n("AnimatorController")]),_:1})]),e("li",null,[t(s,{to:"#timeline-nla-tracks-export"},{default:a(()=>[n("Timeline โ nla tracks export ๐ฌ")]),_:1})])])]),e("li",null,[t(s,{to:"#interactivity"},{default:a(()=>[n("Interactivity ๐")]),_:1}),e("ul",null,[e("li",null,[t(s,{to:"#custom-components"},{default:a(()=>[n("Custom Components")]),_:1})])])]),e("li",null,[t(s,{to:"#lightmapping"},{default:a(()=>[n("Lightmapping ๐ก")]),_:1})]),e("li",null,[t(s,{to:"#texture-compression"},{default:a(()=>[n("Texture Compression")]),_:1})]),e("li",null,[t(s,{to:"#updating"},{default:a(()=>[n("Updating")]),_:1})]),e("li",null,[t(s,{to:"#debugging-reporting-a-problem"},{default:a(()=>[n("Debugging / Reporting a problem")]),_:1}),e("ul",null,[e("li",null,[t(s,{to:"#integrated-bugreporter"},{default:a(()=>[n("Integrated Bugreporter")]),_:1})])])])])]),F,e("p",null,[n("Please note: The current state of the exporter for Blender is in the alpha phase - which means that some features you may know from the Needle Engine Unity Integration may not yet be implemented. "),G,n(" when it comes to deciding which of those features should be prioritizes."),O,n(" If you have feedback for us please let us know in "),e("a",H,[n("discussions"),t(o)]),n(" or in our "),e("a",J,[n("discord community"),t(o)]),n("!")]),e("p",null,[n("If you find bugs or see errors please "),e("a",X,[n("open an issue"),t(o)]),n(" or ask on discord.")]),$,t(c,{href:"https://engine.needle.tools/downloads/blender"},{default:a(()=>[K]),_:1}),Q,t(l,{limit_height:"",src:"/docs/blender/animation.mp4"}),n(),Z,t(l,{limit_height:"",max_height:"188px",src:"/docs/blender/animatorcontroller-create.mp4"}),n(),ee,e("p",null,[n("To create custom components open the workspace via the Needle Project panel and add a "),ne,n(" script file in "),te,n(" inside your web project. Please refer to the "),e("a",ae,[n("scripting documentation"),t(o)]),n(" to learn how to write custom components for Needle Engine.")]),se,oe,ie,e("div",le,[re,e("p",null,[n("You are using an early preview of these features - we recommend creating a backup of your blend file when using Lightmapping at this point in time. Please report problems or errors you encounter in "),e("a",pe,[n("our discord"),t(o)]),n(" ๐")])]),t(l,{limit_height:"",max_height:"800px",src:"/docs/blender/lightmapping.mp4"}),n(),e("blockquote",null,[e("p",null,[n("You can download the original blend file from the video "),e("a",ce,[n("here"),t(o)]),n(".")])]),de,e("p",null,[n("The Needle Engine Build Pipeline automatically compresses textures using ECT1S and UASTC (depending on their usage in materials) when making a production build ("),e("strong",null,[n("requires "),t(p,{to:"/getting-started/#install-these-tools-for-production-builds"},{default:a(()=>[n("toktx")]),_:1}),n(" being installed")]),n("). But you can override or change the compression type per texture in the Material panel.")]),e("p",null,[n("You can modify the compression that is being applied per texture. To override the default compression settings go to the "),ue,n(" tab and open the "),he,n(". There you will find a toggle to override the texture settings per texture used in your material. See the "),t(p,{to:"/deployment.html#how-do-i-choose-between-etc1s-uastc-and-webp-compression"},{default:a(()=>[n("texture compression table")]),_:1}),n(" for a brief overview over the differences between each compression algorithm.")]),me,e("p",null,[n("If you run into any problems we're more than happy to help! Please join "),e("a",be,[n("our discord"),t(o)]),n(" for fast support.")]),ke,ge,fe])}const Te=h(L,[["render",we],["__file","index.html.vue"]]);export{Te as default};
diff --git a/assets/index.html-7104cbee.js b/assets/index.html-7104cbee.js
new file mode 100644
index 000000000..0d60d6f5f
--- /dev/null
+++ b/assets/index.html-7104cbee.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-40fd290b","path":"/community/contributions/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/community: contributions.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-72dcd24e.js b/assets/index.html-72dcd24e.js
new file mode 100644
index 000000000..c8edc5123
--- /dev/null
+++ b/assets/index.html-72dcd24e.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-10c99314","path":"/community/contributions/marwie/camera-video-background/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/marwie: camera video background.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-75b46317.js b/assets/index.html-75b46317.js
new file mode 100644
index 000000000..d7a5d8421
--- /dev/null
+++ b/assets/index.html-75b46317.js
@@ -0,0 +1,37 @@
+import{_ as p}from"./logo-b695892f.js";import{_,M as i,p as m,q as g,N as n,V as o,Q as r,R as e,t,a1 as d}from"./framework-c782e227.js";const w="/docs/imgs/unity-logo.webp",b="/docs/imgs/threejs-logo.webp",y={},f=e("h2",{id:"prerequisites",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#prerequisites","aria-hidden":"true"},"#"),t(" Prerequisites")],-1),x=e("p",null,"Before getting started we recommend you install Nodejs (if you don't have it already)",-1),k=e("br",null,null,-1),v=e("br",null,null,-1),j=e("br",null,null,-1),S=e("br",null,null,-1),N=e("br",null,null,-1),E=e("hr",null,null,-1),T=e("h3",{id:"the-following-tools-are-optional-but-recommended",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#the-following-tools-are-optional-but-recommended","aria-hidden":"true"},"#"),t(" The following tools are optional - "),e("em",null,"but recommended")],-1),C=e("br",null,null,-1),B=e("br",null,null,-1),q=e("br",null,null,-1),I=e("br",null,null,-1),O=e("em",null,"recommend",-1),D=e("br",null,null,-1),U=e("br",null,null,-1),W=e("br",null,null,-1),P=e("p",null,[t("Then continue below at "),e("a",{href:"#needle-engine-with-unity"},"Needle Engine for Unity"),t(" or "),e("a",{href:"#needle-engine-with-blender"},"Needle Engine for Blender")],-1),A=e("br",null,null,-1),F=e("br",null,null,-1),M=e("br",null,null,-1),V=e("br",null,null,-1),L=e("img",{src:w,style:{"max-height":"70px"}},null,-1),R=e("h2",{id:"needle-engine-with-unity",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#needle-engine-with-unity","aria-hidden":"true"},"#"),t(" Needle Engine with Unity")],-1),z=e("p",null,[e("em",null,"We support Unity 2021 and 2022 LTS")],-1),Y=e("strong",null,"Download Needle Engine for Unity",-1),G=e("li",null,[e("p",null,"Drop the downloaded .unitypackage file into a Unity project (or double click to open) and confirm that you want to import it.")],-1),Q=e("li",null,[e("p",null,"Wait for the installation and import to finish.")],-1),H=e("li",null,[e("p",null,'A window may open stating that "A new scoped registry is now available in the Package Manager.". This is our Needle Package registry where packages are downloaded from. You can safely close that window and continue with the next step below.')],-1),K=e("p",null,"Continue....",-1),X=e("strong",null,"Option 1: Explore Samples",-1),J=e("br",null,null,-1),Z=e("em",null,"Needle Engine > Explore Samples",-1),$={href:"https://engine.needle.tools/samples",target:"_blank",rel:"noopener noreferrer"},ee=e("strong",null,"Option 2: Create a new scene from a template",-1),te=e("br",null,null,-1),ne=e("em",null,"File > New Scene",-1),oe=e("br",null,null,-1),le={href:"https://engine.needle.tools/samples/collaborative-sandbox",target:"_blank",rel:"noopener noreferrer"},se={class:"custom-container details"},ie=e("summary",null,"Video: Starting from a fresh Unity project",-1),re=d('Create a new scene from a Scene Template
We provide a number of Scene Templates for quickly starting new projects. These allow you to go from idea to prototype in a few clicks.
Click on File > New Scene
Select one of the templates with (needle) in their name and click Create.
Click Play to install and startup your new web project.
',2),ae={class:"custom-container details"},de=d('Create a new scene from scratch
If you don't want to start from a scene template, you can follow these steps. Effectively, we're going to recreate the "Minimal (Needle)" template that's shipping with the package.
Create a new empty scene
Set up your scene for exporting Add an empty GameObject, name it "Exporter" and add an ExportInfo component to it. In this component you create and quickly access your exported runtime project. It also warns you if any of our packages and modules are outdated or not locally installed in your web project.
Note
By default, the project name matches the name of your scene. If you want to change that, you can enter a Directory Name where you want to create your new runtime project. The path is relative to your Unity project.
',4),ue={start:"3"},ce=e("strong",null,"Choose a web project template",-1),he={href:"https://vitejs.dev/",target:"_blank",rel:"noopener noreferrer"},pe=e("li",null,[e("p",null,"Click Play to install and start your new web project")],-1),_e=e("br",null,null,-1),me=e("br",null,null,-1),ge=e("br",null,null,-1),we=e("img",{src:p,style:{"max-height":"70px"}},null,-1),be=e("h2",{id:"needle-engine-with-blender",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#needle-engine-with-blender","aria-hidden":"true"},"#"),t(" Needle Engine with Blender")],-1),ye=e("p",null,[e("em",null,"We support Blender 3.6 and Blender 4.0")],-1),fe=e("strong",null,"Download Needle Engine for Blender",-1),xe=e("p",null,[e("em",null,[t("With Needle Engine for Blender you can build fully interactive 3D websites running on three.js."),e("br"),t(" They can easily deployed to the web and get optimized automatically by the Needle Engine Build Pipeline (needs Node.js installed)")]),e("br")],-1),ke=e("ul",null,[e("li",null,"The Blender addon is downloaded as a zip file."),e("li",null,[t("In Blender go to "),e("code",null,"File / Settings / Add-ons"),t(" and click the "),e("code",null,"Install"),t(" button.")]),e("li",null,"Then select the downloaded zip to install it.")],-1),ve=e("br",null,null,-1),je=e("br",null,null,-1),Se=e("br",null,null,-1),Ne=e("img",{src:b,style:{"max-height":"70px"}},null,-1),Ee=e("h2",{id:"needle-engine-without-editor",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#needle-engine-without-editor","aria-hidden":"true"},"#"),t(" Needle Engine without Editor")],-1),Te=e("p",null,[t("Needle Engine is built ontop of three.js. If you just want to work with the Needle Engine runtime and don't want to use any Editor integration just yet then go ahead and install it from npm by running:"),e("br"),e("br"),e("code",null,"npm i @needle-tools/engine")],-1),Ce={href:"https://engine.needle.tools/new",target:"_blank",rel:"noopener noreferrer"},Be=e("br",null,null,-1),qe=e("em",null,"It creates a new project on StackBlitz",-1),Ie=d('
Alternatively you can add a prebundled version of Needle Engine to your website:
The local website shows a warning: website not secure
You might see a warning in your browser about SSL Security depending on your local configuration. This is because while the connection is encrypted, by default there's no SSL certificate that the browser can validate. If that happens: click Advanced and Proceed to Site. Now you should see your scene in the browser!
Something is not working as expected? Where can I see logs?
Keep an eye for console warnings! We log useful details about recommended project settings and so on. For example, your project should be set to Linear color space (not Gamma), and we'll log an error to the Unity console if that's not the case.
',8),Oe=e("h2",{id:"what-s-next",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#what-s-next","aria-hidden":"true"},"#"),t(" What's next?")],-1),De=e("li",null,[e("a",{href:"../deployment"},"Deploy your website to the web")],-1),Ue=e("br",null,null,-1),We={href:"https://discord.needle.tools",target:"_blank",rel:"noopener noreferrer"},Pe={href:"https://forum.needle.tools",target:"_blank",rel:"noopener noreferrer"};function Ae(Fe,Me){const a=i("os-link"),l=i("RouterLink"),h=i("ClientOnly"),u=i("needle-button"),s=i("ExternalLinkIcon"),c=i("video-embed");return m(),g("div",null,[f,x,n(h,null,{default:o(()=>[r(' Needle Engine for Unity โ Needle Engine for Blender '),e("p",null,[k,n(a,{windows_url:"https://nodejs.org/dist/v20.9.0/node-v20.9.0-x64.msi",osx_url:"https://nodejs.org/dist/v20.9.0/node-v20.9.0.pkg"},{default:o(()=>[t("Download Nodejs โญ")]),_:1}),v,t(" Node.js is used to preview and build the website that you are creating on your computer."),j,t(" It is also used for uploading (deploy) your website when you are finished and when you want to share it. "),S,N]),E,T,e("p",null,[C,n(a,{windows_url:"https://fwd.needle.tools/needle-engine/toktx/win",osx_url:"https://fwd.needle.tools/needle-engine/toktx/osx",osx_silicon_url:"https://fwd.needle.tools/needle-engine/toktx/osx-silicon"},{default:o(()=>[t("Download Toktx ๐")]),_:1}),B,t(" We use Toktx in production builds (optimized builds) for texture compression (KTX2). You can read more about production builds "),n(l,{to:"/deployment.html#production-builds"},{default:o(()=>[t("here")]),_:1}),t(" in our docs")]),e("p",null,[q,n(a,{windows_url:"https://code.visualstudio.com/sha/download?build=stable&os=win32-x64-user",osx_url:"https://code.visualstudio.com/sha/download?build=stable&os=darwin-universal"},{default:o(()=>[t("Download VSCode ๐")]),_:1}),I,t(" When you plan to edit or write code (js or HTML) then we "),O,t(" that you use VSCode as your code editor.")]),D]),_:1}),U,W,t(" After having installed the tools listed above you might have to restart your machine once! "),P,A,F,r(` | Tool | | |
+| -- | -- | -- |
+| Node.js **(required)** | 16.x or 18.x [Windows](https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi) [MacOS](https://nodejs.org/dist/v18.16.0/node-v18.16.0.pkg) | For running a local development server
+| VS Code *(recommended)* | any version [Windows](https://code.visualstudio.com/sha/download?build=stable&os=win32-x64-user) [MacOS](https://code.visualstudio.com/sha/download?build=stable&os=darwin-universal) | For code editing (optional) |
+| **Supported Editors** | |
+| Unity | 2020.3.16+ 2021.3.9+ 2022.3.0+ [Get Unity Hub](https://unity.com/download) | For setting up your scenes, components, animations... |
+| Blender | 3.3 3.4 3.5 3.6 [Get Blender](https://www.blender.org/download/) | For setting up your scenes, components, animations... |
+ `),r(` ### For optimized builds
+
+| Tool | | |
+| -- | -- | -- |
+| | | |
+| **toktx** | 4.1 [Windows](https://fwd.needle.tools/needle-engine/toktx/win) [MacOS](https://fwd.needle.tools/needle-engine/toktx/osx) [Mac OS Apple Silicon](https://fwd.needle.tools/needle-engine/toktx/osx-silicon) [Other Releases](https://github.com/KhronosGroup/KTX-Software/releases/tag/v4.1.0-rc3) | For texture compression (recommended) You can read more about that [here](./deployment.md#production-builds) in our docs `),M,V,L,R,z,n(u,{large:"",href:"https://engine.needle.tools/downloads/unity"},{default:o(()=>[Y]),_:1}),r(" [Mirror](https://package-installer.glitch.me/v1/installer/needle/com.needle.engine-exporter?registry=https://packages.needle.tools&scope=com.needle&scope=org.khronos) "),e("ul",null,[G,Q,H,e("li",null,[K,e("ul",null,[e("li",null,[e("p",null,[X,J,t(" Select "),Z,t(" to view, open and modify all available "),e("a",$,[t("sample scenes"),n(s)]),t(".")])]),e("li",null,[e("p",null,[ee,te,t(" Select "),ne,t(" and choose from one of the Needle templates."),oe,t(" We recommend the "),e("a",le,[t("Collaborative Sandbox"),n(s)]),t(" template which is a great way to get started with interactivity, multiplayer,and adding assets.")])])])])]),e("details",se,[ie,n(c,{src:"https://www.youtube.com/watch?v=gZX_sqrne8U",limit_height:""}),t(),n(c,{src:"https://www.youtube.com/watch?v=3dB-d1Jo_Mk",limit_height:""})]),re,e("details",ae,[de,e("ol",ue,[e("li",null,[e("p",null,[ce,t(" Now, select a web project template for your project. The default template is based on "),e("a",he,[t("Vite"),n(s)]),t(", a fast web app bundler.")])]),pe])]),_e,me,ge,we,be,ye,n(u,{large:"",href:"https://engine.needle.tools/downloads/blender"},{default:o(()=>[fe]),_:1}),t(),xe,ke,e("p",null,[e("strong",null,[t("Continue reading "),n(l,{to:"/blender/"},{default:o(()=>[t("Needle Engine for Blender")]),_:1})]),t(" for a full feature list and instructions")]),ve,je,Se,Ne,Ee,Te,e("p",null,[t("To quickly test Needle Engine in the browser use "),e("a",Ce,[t("engine.needle.tools/new"),n(s)]),Be,qe]),Ie,e("p",null,[t("Please also have a look at "),n(l,{to:"/faq.html"},{default:o(()=>[t("our FAQ")]),_:1}),t(" if your question is not answered here.")]),r(`
+## Option 1: Quick Start โ Starter Project โก
+1. **Download or Clone this repository**
+ It's set up with the right packages and settings to get you started right away.
+
+ _Clone with HTTPS:_ \`\`https://github.com/needle-tools/needle-engine-support.git\`\`
+ _OR clone with SSH:_ \`\`git@github.com:needle-tools/needle-engine-support.git\`\`
+ _OR download directly:_ Download Repository
+
+
+2. **Open the starter project**
+ Open \`starter/Needle Engine Starter 2020_3\` for a full sandbox project that's ready to run (including a couple of simple example scenes for lightmaps and custom shaders).
+ This is a sandbox builder project! It already comes with multi-player capabilities, and works across mobile, desktop, VR and AR.
+
+3. **Press Play**
+ Make sure the scene CollaborativeSandbox is open, and press Play! This will automatically do some setup steps and start a local server.
+ Once the setup is complete, a browser window will open, and your project is live.
+ From now on, all changes you do in Unity will be immediately visible in your browser.
+
+ > **Note**: Your browser might warn you about an untrusted SSL connection. Don't worry, the connection is still encrypted โ please click "Advance" if your browser asks you to verify that you're sure you want to visit your server.
+
+4. **Make it your own**
+ Add assets and components, play around with lighting, add scripts and logic โ this is your world now!
+ You can also [publish it on the web for free](#deploy-your-project-to-glitch-) so that others can join you.
+`),Oe,e("ul",null,[e("li",null,[n(l,{to:"/export.html"},{default:o(()=>[t("Exporting 3D objects and content")]),_:1})]),e("li",null,[n(l,{to:"/project-structure.html"},{default:o(()=>[t("Project Structure")]),_:1})]),De,e("li",null,[n(l,{to:"/getting-started/typescript-essentials.html"},{default:o(()=>[t("Typescript Essentials")]),_:1})]),e("li",null,[n(l,{to:"/getting-started/for-unity-developers.html"},{default:o(()=>[t("Needle Engine for Unity Developers")]),_:1})]),e("li",null,[n(l,{to:"/scripting.html"},{default:o(()=>[t("Scripting Reference")]),_:1})])]),e("p",null,[t("In case you need more troubleshooting help, please see the "),n(l,{to:"/faq.html"},{default:o(()=>[t("Questions and Answers")]),_:1}),t(" section."),Ue,t(" You can also join our "),e("a",We,[t("Discord Community"),n(s)]),t(" or "),e("a",Pe,[t("Forum"),n(s)])])])}const Re=_(y,[["render",Ae],["__file","index.html.vue"]]);export{Re as default};
diff --git a/assets/index.html-76f967a7.js b/assets/index.html-76f967a7.js
new file mode 100644
index 000000000..995b746da
--- /dev/null
+++ b/assets/index.html-76f967a7.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-289be385","path":"/community/contributions/llllkatjallll/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/contributions: llllkatjallll.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-8e198831.js b/assets/index.html-8e198831.js
new file mode 100644
index 000000000..46df2647f
--- /dev/null
+++ b/assets/index.html-8e198831.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-c2d8b5ac","path":"/blender/","title":"Needle Engine for Blender","lang":"en-US","frontmatter":{"title":"Needle Engine for Blender","editLink":false,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/needle engine for blender.png"}],["meta",{"name":"og:description","content":"---\\nThank you for using Needle Engine for Blender Alpha.\\nWith this addon you can create highly interactive and optimized WebGL and WebXR experiences inside Blender that run using Needle Engine and three.js.\\nYou'll be able to sequence animations, easily lightmap your scenes, add interactivity or create your own scripts written in Typescript or Javascript that run on the web. You own your content!\\nAutomatically export HDRI environment lights directly from blender. Save to reload your local server\\nCreate and export animator statemachines for controlling complex character animations\\n\\nPlease note: The current state of the exporter for Blender is in the alpha phase"}]],"description":"---\\nThank you for using Needle Engine for Blender Alpha.\\nWith this addon you can create highly interactive and optimized WebGL and WebXR experiences inside Blender that run using Needle Engine and three.js.\\nYou'll be able to sequence animations, easily lightmap your scenes, add interactivity or create your own scripts written in Typescript or Javascript that run on the web. You own your content!\\nAutomatically export HDRI environment lights directly from blender. Save to reload your local server\\nCreate and export animator statemachines for controlling complex character animations\\n\\nPlease note: The current state of the exporter for Blender is in the alpha phase"},"headers":[{"level":2,"title":"Preface","slug":"preface","link":"#preface","children":[]},{"level":2,"title":"Download and Installation ๐ฟ","slug":"download-and-installation","link":"#download-and-installation","children":[{"level":3,"title":"Step 1 โข Install Blender 3.6 or 4.0","slug":"step-1-install-blender-3.6-or-4.0","link":"#step-1-install-blender-3.6-or-4.0","children":[]},{"level":3,"title":"Step 2 โข Install Node.js (optional but recommended)","slug":"step-2-install-node.js-optional-but-recommended","link":"#step-2-install-node.js-optional-but-recommended","children":[]},{"level":3,"title":"Step 3 โข Download and Install the addon","slug":"step-3-download-and-install-the-addon","link":"#step-3-download-and-install-the-addon","children":[]}]},{"level":2,"title":"Getting Started ๐ฉ","slug":"getting-started","link":"#getting-started","children":[{"level":3,"title":"Project Panel overview","slug":"project-panel-overview","link":"#project-panel-overview","children":[]}]},{"level":2,"title":"Blender Settings","slug":"blender-settings","link":"#blender-settings","children":[{"level":3,"title":"Color Management","slug":"color-management","link":"#color-management","children":[]}]},{"level":2,"title":"Export","slug":"export","link":"#export","children":[]},{"level":2,"title":"Animation ๐","slug":"animation","link":"#animation","children":[{"level":3,"title":"AnimatorController","slug":"animatorcontroller","link":"#animatorcontroller","children":[]},{"level":3,"title":"Timeline โ nla tracks export ๐ฌ","slug":"timeline-nla-tracks-export","link":"#timeline-nla-tracks-export","children":[]}]},{"level":2,"title":"Interactivity ๐","slug":"interactivity","link":"#interactivity","children":[{"level":3,"title":"Custom Components","slug":"custom-components","link":"#custom-components","children":[]}]},{"level":2,"title":"Lightmapping ๐ก","slug":"lightmapping","link":"#lightmapping","children":[]},{"level":2,"title":"Texture Compression","slug":"texture-compression","link":"#texture-compression","children":[]},{"level":2,"title":"Updating","slug":"updating","link":"#updating","children":[]},{"level":2,"title":"Debugging / Reporting a problem","slug":"debugging-reporting-a-problem","link":"#debugging-reporting-a-problem","children":[{"level":3,"title":"Integrated Bugreporter","slug":"integrated-bugreporter","link":"#integrated-bugreporter","children":[]}]}],"git":{},"filePathRelative":"blender/index.md"}`);export{e as data};
diff --git a/assets/index.html-8e345e12.js b/assets/index.html-8e345e12.js
new file mode 100644
index 000000000..5fedc2c9c
--- /dev/null
+++ b/assets/index.html-8e345e12.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-ccdc4da0","path":"/getting-started/","title":"Getting Started & Installation","lang":"en-US","frontmatter":{"lang":"en-US","title":"Getting Started & Installation","next":"../project-structure.md","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/getting started & installation.png"}],["meta",{"name":"og:description","content":"---\\nBefore getting started we recommend you install Nodejs (if you don't have it already)"}]],"description":"---\\nBefore getting started we recommend you install Nodejs (if you don't have it already)"},"headers":[{"level":2,"title":"Prerequisites","slug":"prerequisites","link":"#prerequisites","children":[{"level":3,"title":"The following tools are optional - but recommended","slug":"the-following-tools-are-optional-but-recommended","link":"#the-following-tools-are-optional-but-recommended","children":[]}]},{"level":2,"title":"Needle Engine with Unity","slug":"needle-engine-with-unity","link":"#needle-engine-with-unity","children":[]},{"level":2,"title":"Needle Engine with Blender","slug":"needle-engine-with-blender","link":"#needle-engine-with-blender","children":[]},{"level":2,"title":"Needle Engine without Editor","slug":"needle-engine-without-editor","link":"#needle-engine-without-editor","children":[]},{"level":2,"title":"Questions?","slug":"questions","link":"#questions","children":[]},{"level":2,"title":"What's next?","slug":"what-s-next","link":"#what-s-next","children":[]}],"git":{},"filePathRelative":"getting-started/index.md"}`);export{e as data};
diff --git a/assets/index.html-8eb2326b.js b/assets/index.html-8eb2326b.js
new file mode 100644
index 000000000..dc7dc1d2b
--- /dev/null
+++ b/assets/index.html-8eb2326b.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-fd5a4508","path":"/community/contributions/krisrok/always-open-in-specific-browser/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/krisrok: always open in specific browser.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-9603f594.js b/assets/index.html-9603f594.js
new file mode 100644
index 000000000..d01de56fc
--- /dev/null
+++ b/assets/index.html-9603f594.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-a305d722","path":"/community/contributions/web3kev/squeeze-to-scale-object-or-world-in-vr/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/web3kev: squeeze to scale object or world in vr.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-9d6db80a.js b/assets/index.html-9d6db80a.js
new file mode 100644
index 000000000..3ff6d0ea8
--- /dev/null
+++ b/assets/index.html-9d6db80a.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-28e23eea","path":"/community/contributions/robyer1/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/contributions: robyer1.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-a5b8883f.js b/assets/index.html-a5b8883f.js
new file mode 100644
index 000000000..07ec7d427
--- /dev/null
+++ b/assets/index.html-a5b8883f.js
@@ -0,0 +1,46 @@
+import{_ as l,M as a,p,q as i,N as t,V as e,R as n,t as s}from"./framework-c782e227.js";const u={},r=n("p",null,"If you want to set a fallback material for an object that will be exported as USDZ (for AR-mode on iOS), you can add this script to the object, which material should be replaced.",-1),k=n("p",null,"This is especially useful if you use custom shaders in your scene (they are visible on Desktop+WebXR, but not in AR on iOS).",-1),d=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Behaviour"),n("span",{class:"token punctuation"},","),s(" GameObject"),n("span",{class:"token punctuation"},","),s(" Renderer"),n("span",{class:"token punctuation"},","),s(" USDZExporter"),n("span",{class:"token punctuation"},","),s(" serializable "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Material"),n("span",{class:"token punctuation"},","),s(" Object3D "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"three"'),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"FallbackMaterial"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),s("Material"),n("span",{class:"token punctuation"},")"),s(`
+ fallbackMaterial`),n("span",{class:"token operator"},"!"),n("span",{class:"token operator"},":"),s(" Material"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(" originalMaterial"),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" Material"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"private"),s(" usdzExporter"),n("span",{class:"token operator"},"!"),n("span",{class:"token operator"},":"),s(" USDZExporter"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token function"},"onEnable"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("usdzExporter "),n("span",{class:"token operator"},"="),s(" GameObject"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"findObjectOfType"),n("span",{class:"token punctuation"},"("),s("USDZExporter"),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},"!"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"subscribeToBeforeExportEvent"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"onDisable"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"unsubscribeFromBeforeExportEvent"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(),n("span",{class:"token function"},"subscribeToBeforeExportEvent"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("usdzExporter"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"addEventListener"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"before-export"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("onBeforeExport"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("usdzExporter"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"addEventListener"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"after-export"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("onAfterExport"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(),n("span",{class:"token function"},"unsubscribeFromBeforeExportEvent"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("usdzExporter"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"removeEventListener"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"before-export"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("onBeforeExport"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("usdzExporter"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"removeEventListener"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"after-export"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("onAfterExport"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+
+ `),n("span",{class:"token function-variable function"},"onBeforeExport"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"=>"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"onBeforeExport"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" renderer "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"getComponent"),n("span",{class:"token punctuation"},"("),s("Renderer"),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},"!"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("originalMaterial "),n("span",{class:"token operator"},"="),s(" renderer"),n("span",{class:"token punctuation"},"."),s("sharedMaterial"),n("span",{class:"token punctuation"},";"),s(`
+ renderer`),n("span",{class:"token punctuation"},"."),s("sharedMaterial "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("fallbackMaterial"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function-variable function"},"onAfterExport"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"=>"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"onAfterExport"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" renderer "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"getComponent"),n("span",{class:"token punctuation"},"("),s("Renderer"),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},"!"),n("span",{class:"token punctuation"},";"),s(`
+ renderer`),n("span",{class:"token punctuation"},"."),s("sharedMaterial "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("originalMaterial"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1);function b(m,v){const o=a("contribution-preview"),c=a("contributions-author");return p(),i("div",null,[t(c,{overviewLink:"/docs/community/contributions",name:"llllkatjallll",url:"https://github.com/llllkatjallll",profileImage:"https://avatars.githubusercontent.com/u/38395689?s=100&u=7ce0fef973c4819c4f07823568d6f6061abfe410&v=4",githubUrl:"https://github.com/llllkatjallll"},{default:e(()=>[t(o,{title:"Set fallback material for USDZ exporter",pageUrl:"/docs/community/contributions/llllkatjallll/set-fallback-material-for-usdz-exporter"},{default:e(()=>[r,k,d]),_:1})]),_:1})])}const h=l(u,[["render",b],["__file","index.html.vue"]]);export{h as default};
diff --git a/assets/index.html-c109366f.js b/assets/index.html-c109366f.js
new file mode 100644
index 000000000..fb905368f
--- /dev/null
+++ b/assets/index.html-c109366f.js
@@ -0,0 +1,46 @@
+import{_ as a,M as t,p as e,q as p,N as o,R as n,a1 as c}from"./framework-c782e227.js";const i={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=c(`
If you want to set a fallback material for an object that will be exported as USDZ (for AR-mode on iOS), you can add this script to the object, which material should be replaced.
This is especially useful if you use custom shaders in your scene (they are visible on Desktop+WebXR, but not in AR on iOS).
`,3);function r(k,d){const s=t("contribution-header");return e(),p("div",null,[l,o(s,{url:"https://github.com/llllkatjallll",author:"llllkatjallll",page:"/docs/community/contributions/llllkatjallll",profileImage:"https://avatars.githubusercontent.com/u/38395689?s=100&u=7ce0fef973c4819c4f07823568d6f6061abfe410&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/184",title:"Set fallback material for USDZ exporter",gradient:"True"}),u])}const b=a(i,[["render",r],["__file","index.html.vue"]]);export{b as default};
diff --git a/assets/index.html-d38b3fe7.js b/assets/index.html-d38b3fe7.js
new file mode 100644
index 000000000..5ba30adaa
--- /dev/null
+++ b/assets/index.html-d38b3fe7.js
@@ -0,0 +1,474 @@
+import{_ as l,M as o,p,q as i,N as a,V as t,R as n,t as s}from"./framework-c782e227.js";const u={},k=n("p",null,"The following code will enable Quest users (haven't tested with other devices) to move up and down with the right-joystick`s y axis. (the x axis being used for snap-turns).",-1),r=n("p",null,"This code will interfere with the teleport script when accidentally pointing towards an object and trying to move up. It is recommended to remove the teleport script for that matter.",-1),d=n("p",null,"You can place this script anywhere.",-1),b=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Behaviour"),n("span",{class:"token punctuation"},","),s(" WebXR"),n("span",{class:"token punctuation"},","),s(" GameObject"),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Vector3"),n("span",{class:"token punctuation"},","),s("Quaternion"),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"three"'),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Mathf "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"VerticalMove"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(" webXR"),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" WebXR"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"private"),s(" joystickY"),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),n("span",{class:"token builtin"},"number"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"private"),s(" worldRot"),n("span",{class:"token operator"},":"),s(" Quaternion "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Quaternion"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token function"},"start"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token keyword"},"void"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token keyword"},"let"),s(" _webxr"),n("span",{class:"token operator"},"="),s("GameObject"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"findObjectOfType"),n("span",{class:"token punctuation"},"("),s("WebXR"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),s("_webxr"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token operator"},"="),s("_webxr"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"webxr found"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+
+ `),n("span",{class:"token function"},"update"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("isInVR"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//get y value from right joystick"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"verticalMove"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"verticalMove"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),n("span",{class:"token keyword"},"void"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token operator"},"?."),s("RightController"),n("span",{class:"token operator"},"?."),s("input"),n("span",{class:"token operator"},"?."),s("gamepad"),n("span",{class:"token operator"},"?."),s("axes"),n("span",{class:"token punctuation"},"["),n("span",{class:"token number"},"3"),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("joystickY"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token punctuation"},"."),s("RightController"),n("span",{class:"token punctuation"},"."),s("input"),n("span",{class:"token punctuation"},"."),s("gamepad"),n("span",{class:"token punctuation"},"."),s("axes"),n("span",{class:"token punctuation"},"["),n("span",{class:"token number"},"3"),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"const"),s(" speedFactor "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"3"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" powFactor "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"2"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" speed "),n("span",{class:"token operator"},"="),s(" Mathf"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"clamp01"),n("span",{class:"token punctuation"},"("),n("span",{class:"token number"},"2"),s(),n("span",{class:"token operator"},"*"),s(),n("span",{class:"token number"},"2"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"const"),s(" verticalDir "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("joystickY "),n("span",{class:"token operator"},"<"),s(),n("span",{class:"token number"},"0"),s(),n("span",{class:"token operator"},"?"),s(),n("span",{class:"token number"},"1"),s(),n("span",{class:"token operator"},":"),s(),n("span",{class:"token operator"},"-"),n("span",{class:"token number"},"1"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"let"),s(" vertical "),n("span",{class:"token operator"},"="),s(" Math"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"pow"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("joystickY"),n("span",{class:"token punctuation"},","),s(" powFactor"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ vertical `),n("span",{class:"token operator"},"*="),s(" verticalDir"),n("span",{class:"token punctuation"},";"),s(`
+ vertical `),n("span",{class:"token operator"},"*="),s(" speed"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token punctuation"},"."),s("Rig"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"getWorldQuaternion"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("worldRot"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"let"),s(" movementVector"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Vector3"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ movementVector`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"set"),n("span",{class:"token punctuation"},"("),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},","),s(" vertical"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ movementVector`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"applyQuaternion"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token punctuation"},"."),s("TransformOrientation"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ movementVector`),n("span",{class:"token punctuation"},"."),s("x "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},";"),s(`
+ movementVector`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"applyQuaternion"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("worldRot"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ movementVector`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"multiplyScalar"),n("span",{class:"token punctuation"},"("),s("speedFactor "),n("span",{class:"token operator"},"*"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("time"),n("span",{class:"token punctuation"},"."),s("deltaTime"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token punctuation"},"."),s("Rig"),n("span",{class:"token punctuation"},"."),s("position"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"add"),n("span",{class:"token punctuation"},"("),s("movementVector"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+
+
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),m=n("p",null,"The following code enables you to use both controllers in VR (tested on Quest) and scale the player's perspective (XRRig) by squeezing the grab triggers and moving the controllers closer (pinch out) or further apart (pinch in). The boolean allowWorldScaling has to be ticked in unity for that to work.",-1),v=n("p",null,"Upon selecting a draggable object (Drag controls script), the player can scale up or down that object, while keeping the finger on the trigger and squeezing both grab buttons and moving the hands closer or apart.",-1),w=n("p",null,"The current script enables you to visually see the scale. Create a world canvas with a text component as a child. Assign the world canvas to scaleTextObject and the text to scaleText. scaleTextObject will then spawn in front of the player and follow the head movement whenever scaling.",-1),h=n("p",null,"At the moment the position of the hands (controllers) is done by finding the avatar's hands. I couldn't make it work otherwise. If you find a better way please share.",-1),y=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Behaviour"),n("span",{class:"token punctuation"},","),s(" WebXR"),n("span",{class:"token punctuation"},","),s("serializeable"),n("span",{class:"token punctuation"},","),s(" WebXREvent"),n("span",{class:"token punctuation"},","),s("WebXRAvatar"),n("span",{class:"token punctuation"},","),s("GameObject"),n("span",{class:"token punctuation"},","),s(" AvatarMarker"),n("span",{class:"token punctuation"},","),s("Text"),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Object3D"),n("span",{class:"token punctuation"},","),s(" Vector3"),n("span",{class:"token punctuation"},","),s("Quaternion"),n("span",{class:"token punctuation"},","),s("PerspectiveCamera"),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"three"'),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"SqueezeScale"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+
+ `),n("span",{class:"token keyword"},"private"),s(" webXR"),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" WebXR"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(" selectedObj"),n("span",{class:"token operator"},":"),s(" Object3D"),n("span",{class:"token operator"},"|"),s(),n("span",{class:"token keyword"},"null"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"null"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializeable")]),n("span",{class:"token punctuation"},"("),s("Object3D"),n("span",{class:"token punctuation"},")"),s(`
+ scaleTextObject`),n("span",{class:"token operator"},":"),s(" Object3D"),n("span",{class:"token operator"},"|"),s(),n("span",{class:"token keyword"},"null"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"null"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializeable")]),n("span",{class:"token punctuation"},"("),s("Text"),n("span",{class:"token punctuation"},")"),s(`
+ scaleText`),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" Text"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"public"),s(" allowWorldScaling"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token builtin"},"boolean"),s(),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(" leftSqueeze"),n("span",{class:"token operator"},":"),n("span",{class:"token builtin"},"boolean"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"private"),s(" rightSqueeze"),n("span",{class:"token operator"},":"),n("span",{class:"token builtin"},"boolean"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(" bothSqueezeStarted"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(" rigScaleUpdated"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(" initialDistance"),n("span",{class:"token operator"},":"),n("span",{class:"token builtin"},"number"),n("span",{class:"token operator"},"="),n("span",{class:"token number"},"1"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"private"),s(" initialScale"),n("span",{class:"token operator"},":"),n("span",{class:"token builtin"},"number"),n("span",{class:"token operator"},"="),n("span",{class:"token number"},"1"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"private"),s(" newScale"),n("span",{class:"token operator"},":"),n("span",{class:"token builtin"},"number"),s(),n("span",{class:"token operator"},"|"),s(),n("span",{class:"token keyword"},"null"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"null"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(" leftHand"),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s("Object3D"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"private"),s(" rightHand"),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s("Object3D"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"private"),s(" head"),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s("Object3D"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token function"},"start"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token keyword"},"void"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token keyword"},"let"),s(" _webxr"),n("span",{class:"token operator"},"="),s("GameObject"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"findObjectOfType"),n("span",{class:"token punctuation"},"("),s("WebXR"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),s("_webxr"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token operator"},"="),s("_webxr"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"webxr found"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token comment"},"//Wait for XR Session"),s(`
+ WebXR`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"addEventListener"),n("span",{class:"token punctuation"},"("),s("WebXREvent"),n("span",{class:"token punctuation"},"."),s("XRStarted"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"=>"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//listen to squeeze events"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("xrSession"),n("span",{class:"token operator"},"?."),n("span",{class:"token function"},"addEventListener"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"squeezestart"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token punctuation"},"("),s("event"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"=>"),s(),n("span",{class:"token punctuation"},"{"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"onSqueezeEvent"),n("span",{class:"token punctuation"},"("),s("event"),n("span",{class:"token punctuation"},","),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("xrSession"),n("span",{class:"token operator"},"?."),n("span",{class:"token function"},"addEventListener"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"squeezeend"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token punctuation"},"("),s("event"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"=>"),s(),n("span",{class:"token punctuation"},"{"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"onSqueezeEvent"),n("span",{class:"token punctuation"},"("),s("event"),n("span",{class:"token punctuation"},","),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+
+ `),n("span",{class:"token function"},"onSqueezeEvent"),n("span",{class:"token punctuation"},"("),s("event"),n("span",{class:"token operator"},":"),s(" XRInputSourceEvent"),n("span",{class:"token punctuation"},","),s(" status"),n("span",{class:"token operator"},":"),n("span",{class:"token builtin"},"boolean"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),s("event"),n("span",{class:"token punctuation"},"."),s("inputSource"),n("span",{class:"token punctuation"},"."),s("handedness"),n("span",{class:"token operator"},"==="),n("span",{class:"token string"},'"right"'),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("rightSqueeze"),n("span",{class:"token operator"},"="),s("status"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),s("event"),n("span",{class:"token punctuation"},"."),s("inputSource"),n("span",{class:"token punctuation"},"."),s("handedness"),n("span",{class:"token operator"},"==="),n("span",{class:"token string"},'"left"'),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("leftSqueeze"),n("span",{class:"token operator"},"="),s("status"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"update"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("isInVR"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//cache object selected if any"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"objectGrab"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//if both grips are squeezed "),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("leftSqueeze "),n("span",{class:"token operator"},"&&"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("rightSqueeze"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//if object is selected either in the left or right controller (only one)"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("selectedObj"),n("span",{class:"token operator"},"!="),n("span",{class:"token keyword"},"null"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//after initial distance value has been set"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("bothSqueezeStarted"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//get current distance between controllers"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" scaleValue"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"calculateDistance"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},'//get distance change since beginning of squeeze to get a "pinch in/out" effect'),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("initialScale"),n("span",{class:"token operator"},"+"),s("scaleValue"),n("span",{class:"token operator"},"-"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("initialDistance"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//avoid 0 and negative scales"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token operator"},"<"),n("span",{class:"token number"},"0.001"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"{"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token operator"},"="),n("span",{class:"token number"},"0.001"),n("span",{class:"token punctuation"},";"),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token comment"},"// scale object according to new distance since initial distance"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("selectedObj"),n("span",{class:"token punctuation"},"."),s("scale"),n("span",{class:"token punctuation"},"."),s("x"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("selectedObj"),n("span",{class:"token punctuation"},"."),s("scale"),n("span",{class:"token punctuation"},"."),s("y"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("selectedObj"),n("span",{class:"token punctuation"},"."),s("scale"),n("span",{class:"token punctuation"},"."),s("z"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"showVisual"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token punctuation"},","),n("span",{class:"token string"},'"Object :"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token keyword"},"else"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//get initial distance value (only once at a new squeeze both hands event)"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("bothSqueezeStarted"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("initialDistance"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"calculateDistance"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//cache object's initial scale"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("initialScale"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("selectedObj"),n("span",{class:"token punctuation"},"."),s("scale"),n("span",{class:"token punctuation"},"."),s("x"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token keyword"},"else"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//scale world ?"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token operator"},"?."),s("Rig "),n("span",{class:"token operator"},"&&"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("allowWorldScaling"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//after initial distance value has been set"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("bothSqueezeStarted"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//get current distance between controllers"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" scaleValue"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"calculateDistance"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},'//get distance change since beginning of squeeze to get a "pinch in/out" effect'),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("initialScale"),n("span",{class:"token operator"},"+"),s("scaleValue"),n("span",{class:"token operator"},"-"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("initialDistance"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//avoid 0 and negative scales"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token operator"},"<"),n("span",{class:"token number"},"0.001"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"{"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token operator"},"="),n("span",{class:"token number"},"0.001"),n("span",{class:"token punctuation"},";"),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"showVisual"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token string"},'"World :"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("rigScaleUpdated"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token keyword"},"else"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//get initial distance value (only once at a new squeeze both hands event)"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("bothSqueezeStarted"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("initialDistance"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"calculateDistance"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//cache object's initial scale"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("initialScale"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token punctuation"},"."),s("Rig"),n("span",{class:"token punctuation"},"."),s("scale"),n("span",{class:"token punctuation"},"."),s("x"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token keyword"},"else"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//reset values"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("bothSqueezeStarted"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//if world has been scaled, scale rig accordingly at the end of squeezing and once only"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token operator"},"?."),s("Rig "),n("span",{class:"token operator"},"&&"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("rigScaleUpdated "),n("span",{class:"token operator"},"&&"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//change rig scale"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token punctuation"},"."),s("Rig"),n("span",{class:"token punctuation"},"."),s("scale"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"set"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token punctuation"},"."),s("Rig"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"updateMatrixWorld"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"const"),s(" cam "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("mainCamera "),n("span",{class:"token keyword"},"as"),s(" PerspectiveCamera"),n("span",{class:"token punctuation"},";"),s(`
+ cam`),n("span",{class:"token punctuation"},"."),s("near"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token operator"},">"),n("span",{class:"token number"},"2"),n("span",{class:"token operator"},"?"),n("span",{class:"token number"},"0.0001"),n("span",{class:"token operator"},":"),n("span",{class:"token number"},"0.2"),n("span",{class:"token punctuation"},";"),s(`
+ cam`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"updateProjectionMatrix"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//reset"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("rigScaleUpdated"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("scaleTextObject"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("scaleTextObject"),n("span",{class:"token punctuation"},"."),s("visible"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("newScale"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"null"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(),n("span",{class:"token function"},"calculateDistance"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),n("span",{class:"token builtin"},"number"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"let"),s(" distance"),n("span",{class:"token operator"},"="),n("span",{class:"token number"},"1"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("leftHand "),n("span",{class:"token operator"},"&&"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("rightHand"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token keyword"},"const"),s(" left"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("leftHand"),n("span",{class:"token punctuation"},"."),s("position"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" right"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("rightHand"),n("span",{class:"token punctuation"},"."),s("position"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// Calculate the difference between the positions"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" dx "),n("span",{class:"token operator"},"="),s(" left"),n("span",{class:"token punctuation"},"."),s("x "),n("span",{class:"token operator"},"-"),s(" right"),n("span",{class:"token punctuation"},"."),s("x"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" dy "),n("span",{class:"token operator"},"="),s(" left"),n("span",{class:"token punctuation"},"."),s("y "),n("span",{class:"token operator"},"-"),s(" right"),n("span",{class:"token punctuation"},"."),s("y"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" dz "),n("span",{class:"token operator"},"="),s(" left"),n("span",{class:"token punctuation"},"."),s("z "),n("span",{class:"token operator"},"-"),s(" right"),n("span",{class:"token punctuation"},"."),s("z"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"// Calculate the distance using the Euclidean distance formula"),s(`
+ distance `),n("span",{class:"token operator"},"="),s(" Math"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"sqrt"),n("span",{class:"token punctuation"},"("),s("dx "),n("span",{class:"token operator"},"*"),s(" dx "),n("span",{class:"token operator"},"+"),s(" dy "),n("span",{class:"token operator"},"*"),s(" dy "),n("span",{class:"token operator"},"+"),s(" dz "),n("span",{class:"token operator"},"*"),s(" dz"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token keyword"},"else"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//set positions of controllers from your avatar (only once)"),s(`
+ `),n("span",{class:"token keyword"},"let"),s(" allAvatars "),n("span",{class:"token operator"},"="),s(" AvatarMarker"),n("span",{class:"token punctuation"},"."),s("instances"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),s("allAvatars"),n("span",{class:"token punctuation"},"."),s("length"),n("span",{class:"token operator"},">"),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"for"),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"let"),s(" i"),n("span",{class:"token operator"},"="),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},";"),s("i"),n("span",{class:"token operator"},"<"),s("allAvatars"),n("span",{class:"token punctuation"},"."),s("length"),n("span",{class:"token punctuation"},";"),s("i"),n("span",{class:"token operator"},"++"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),s("allAvatars"),n("span",{class:"token punctuation"},"["),s("i"),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"isLocalAvatar"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" av"),n("span",{class:"token operator"},"="),s("allAvatars"),n("span",{class:"token punctuation"},"["),s("i"),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},"."),s("avatar "),n("span",{class:"token keyword"},"as"),s(" WebXRAvatar"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),s("av"),n("span",{class:"token operator"},"!="),n("span",{class:"token keyword"},"null"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("leftHand"),n("span",{class:"token operator"},"="),s("av"),n("span",{class:"token punctuation"},"."),s("handLeft "),n("span",{class:"token keyword"},"as"),s(" Object3D"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("rightHand"),n("span",{class:"token operator"},"="),s("av"),n("span",{class:"token punctuation"},"."),s("handRight "),n("span",{class:"token keyword"},"as"),s(" Object3D"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("head "),n("span",{class:"token operator"},"="),s(" av"),n("span",{class:"token punctuation"},"."),s("head "),n("span",{class:"token keyword"},"as"),s(" Object3D"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"return"),s(" distance"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"showVisual"),n("span",{class:"token punctuation"},"("),s("scale"),n("span",{class:"token operator"},":"),n("span",{class:"token builtin"},"number"),n("span",{class:"token punctuation"},","),s(" mesg"),n("span",{class:"token operator"},":"),n("span",{class:"token builtin"},"string"),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),n("span",{class:"token keyword"},"void"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("scaleTextObject "),n("span",{class:"token operator"},"&&"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("head "),n("span",{class:"token operator"},"&&"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("scaleText"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("scaleTextObject"),n("span",{class:"token punctuation"},"."),s("visible"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"const"),s(" offset "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Vector3"),n("span",{class:"token punctuation"},"("),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},","),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},","),n("span",{class:"token number"},"7"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ offset`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"applyQuaternion"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("head"),n("span",{class:"token punctuation"},"."),s("quaternion"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("scaleTextObject"),n("span",{class:"token punctuation"},"."),s("position"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"copy"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("head"),n("span",{class:"token punctuation"},"."),s("position"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"add"),n("span",{class:"token punctuation"},"("),s("offset"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"const"),s(" roundedNum"),n("span",{class:"token operator"},"="),s(),n("span",{class:"token operator"},"+"),s("scale"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"toFixed"),n("span",{class:"token punctuation"},"("),n("span",{class:"token number"},"3"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("scaleText"),n("span",{class:"token punctuation"},"."),s("text"),n("span",{class:"token operator"},"="),s("mesg"),n("span",{class:"token operator"},"+"),n("span",{class:"token string"},'" "'),n("span",{class:"token operator"},"+"),s("roundedNum"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+
+ `),n("span",{class:"token function"},"objectGrab"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),n("span",{class:"token keyword"},"void"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token operator"},"?."),s("RightController"),n("span",{class:"token operator"},"?."),s("grabbed"),n("span",{class:"token operator"},"?."),s("selected"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("selectedObj "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token punctuation"},"."),s("RightController"),n("span",{class:"token punctuation"},"."),s("grabbed"),n("span",{class:"token punctuation"},"."),s("selected"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token keyword"},"else"),s(),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token operator"},"?."),s("LeftController"),n("span",{class:"token operator"},"?."),s("grabbed"),n("span",{class:"token operator"},"?."),s("selected"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("selectedObj "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("webXR"),n("span",{class:"token punctuation"},"."),s("LeftController"),n("span",{class:"token punctuation"},"."),s("grabbed"),n("span",{class:"token punctuation"},"."),s("selected"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token keyword"},"else"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("selectedObj"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"null"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),f=n("p",null,"In a multiuser session, typically objects are instantiated using instantiateSynced as such:",-1),g=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Behaviour"),n("span",{class:"token punctuation"},","),s("GameObject"),n("span",{class:"token punctuation"},","),s("serializable"),n("span",{class:"token punctuation"},","),s("InstantiateOptions"),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Vector3"),n("span",{class:"token punctuation"},","),s(" Object3D"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"three"'),n("span",{class:"token punctuation"},";"),s(`
+
+`),n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"InstantiateObjectForAll"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(`
+`),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),s("Object3D"),n("span",{class:"token punctuation"},")"),s(`
+ myPrefab`),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" GameObject"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token function"},"makeObject"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),n("span",{class:"token keyword"},"void"),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" options "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"InstantiateOptions"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ options`),n("span",{class:"token punctuation"},"."),s("context "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},";"),s(`
+ options`),n("span",{class:"token punctuation"},"."),s("position "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Vector3"),n("span",{class:"token punctuation"},"("),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},","),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},","),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ GameObject`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"instantiateSynced"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("myPrefab"),n("span",{class:"token punctuation"},","),s(" options"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token keyword"},"as"),s(" GameObject"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),S=n("p",null,"My particular use-case was for generating programmatically a random scene made of cubes, and that scene had to be the same for all users of the same room. I had used the example above but for some unknown reasons sometimes the scenes were partially rendered when instantiating simultaneously >400 objects. @Marcel of Needle suggested to generate a seed (position of all objects in the scene) and send that seed instead using :",-1),x=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("connection"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"send"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),j=n("p",null,"All users using :",-1),_=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("connection"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"beginListen"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),R=n("p",null,"would receive any seed previously sent, upon joining the same room, allowing them to instantiate cubes according to that seed (array of Vector3).",-1),O=n("p",null,"Here is a script illustrating the use of the send method and the beginListen counterpart:",-1),z=n("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[n("pre",{class:"language-typescript"},[n("code",null,[s(`
+`),n("span",{class:"token comment"},"//This is an example of sending the seed of a randomly generated scene made of cubes, for all other instances logging into the same room to create the same scene."),s(`
+
+`),n("span",{class:"token comment"},"//This script requires a prefab (e.g. a 1x1x1 Cube)"),s(`
+`),n("span",{class:"token comment"},"//This script will generate and build randomly positioned cubes (random walk) as a child of the object it is attached to. "),s(`
+`),n("span",{class:"token comment"},"//The generateSeed() method is in this script called via a button. The button is deactivated once the seed has been transmitted."),s(`
+`),n("span",{class:"token comment"},"//Any users joining the same room will receive the seed and build the exact same scene"),s(`
+
+
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Behaviour"),n("span",{class:"token punctuation"},","),s("GameObject"),n("span",{class:"token punctuation"},","),s("serializable"),n("span",{class:"token punctuation"},","),s("InstantiateOptions"),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@needle-tools/engine"'),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Vector3"),n("span",{class:"token punctuation"},","),s(" Object3D "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"three"'),n("span",{class:"token punctuation"},";"),s(`
+
+
+`),n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"NetworkedSeed"),s(),n("span",{class:"token keyword"},"extends"),s(),n("span",{class:"token class-name"},"Behaviour"),s(`
+`),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),s("Object3D"),n("span",{class:"token punctuation"},")"),s(`
+ prefab`),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" GameObject"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token decorator"},[n("span",{class:"token at operator"},"@"),n("span",{class:"token function"},"serializable")]),n("span",{class:"token punctuation"},"("),s("Object3D"),n("span",{class:"token punctuation"},")"),s(`
+ generateButton`),n("span",{class:"token operator"},"?"),n("span",{class:"token operator"},":"),s(" Object3D"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"public"),s(" seedSize"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token builtin"},"number"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token number"},"30"),n("span",{class:"token punctuation"},";"),s(`
+
+ seed`),n("span",{class:"token operator"},":"),s(" Vector3"),n("span",{class:"token punctuation"},"["),n("span",{class:"token punctuation"},"]"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"["),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token function"},"onEnable"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token keyword"},"void"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("connection"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"beginListen"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"mySeed"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("onDataReceived"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("generateButton"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("generateButton"),n("span",{class:"token punctuation"},"."),s("visible"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token function"},"onDisable"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token keyword"},"void"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("connection"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"stopListen"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"mySeed"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("onDataReceived"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function-variable function"},"onDataReceived"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"("),s("data"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token builtin"},"any"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"=>"),s(),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"Received data:"'),n("span",{class:"token punctuation"},","),s(" data"),n("span",{class:"token punctuation"},"."),s("mySeed"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"."),s("length"),n("span",{class:"token operator"},"==="),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token comment"},"//prevent other generations of the seed"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("generateButton"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("generateButton"),n("span",{class:"token punctuation"},"."),s("visible"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token operator"},"="),s("data"),n("span",{class:"token punctuation"},"."),s("mySeed"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token comment"},"//build scene"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"buildScene"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},";"),s(`
+
+
+ `),n("span",{class:"token comment"},"//generate and send seed to all from the button generateButton"),s(`
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token function"},"generateSeed"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),n("span",{class:"token keyword"},"void"),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"."),s("length"),n("span",{class:"token operator"},"=="),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token comment"},"//no seed found => generate one"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"["),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" uniquePositions "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},[s("Set"),n("span",{class:"token operator"},"<"),n("span",{class:"token builtin"},"string"),n("span",{class:"token operator"},">")]),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//start at origin"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" startPosition "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Vector3"),n("span",{class:"token punctuation"},"("),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"push"),n("span",{class:"token punctuation"},"("),s("startPosition"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"clone"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ uniquePositions`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"add"),n("span",{class:"token punctuation"},"("),s("startPosition"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"toArray"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"toString"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//go for a random walk of length : seedSize"),s(`
+ `),n("span",{class:"token keyword"},"while"),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"."),s("length "),n("span",{class:"token operator"},"<"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seedSize"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" lastPosition "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"["),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"."),s("length "),n("span",{class:"token operator"},"-"),s(),n("span",{class:"token number"},"1"),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"let"),s(" newPosition"),n("span",{class:"token operator"},":"),s(" Vector3"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//walk and add position, making sure they are unique"),s(`
+ `),n("span",{class:"token keyword"},"do"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" direction "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"getRandomDirection"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ newPosition `),n("span",{class:"token operator"},"="),s(" lastPosition"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"clone"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"add"),n("span",{class:"token punctuation"},"("),s("direction"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"while"),s(),n("span",{class:"token punctuation"},"("),s("uniquePositions"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"has"),n("span",{class:"token punctuation"},"("),s("newPosition"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"toArray"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"toString"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"push"),n("span",{class:"token punctuation"},"("),s("newPosition"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"clone"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ uniquePositions`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"add"),n("span",{class:"token punctuation"},"("),s("newPosition"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"toArray"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"toString"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token comment"},"//send the seed to all on the server"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"sendSeed"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token comment"},"//prevent other generations of the seed"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("generateButton"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("generateButton"),n("span",{class:"token punctuation"},"."),s("visible"),n("span",{class:"token operator"},"="),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token comment"},"//build scene locally"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"buildScene"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(),n("span",{class:"token function"},"sendSeed"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),n("span",{class:"token keyword"},"void"),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"."),s("length"),n("span",{class:"token operator"},"!="),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},"."),s("connection"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"send"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"mySeed"'),n("span",{class:"token punctuation"},","),n("span",{class:"token punctuation"},"{"),s("guid"),n("span",{class:"token operator"},":"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("guid"),n("span",{class:"token punctuation"},","),s(" mySeed"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"------ SEED SENT -------"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token function"},"buildScene"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),n("span",{class:"token keyword"},"void"),n("span",{class:"token punctuation"},"{"),s(`
+
+ `),n("span",{class:"token comment"},"//check if the seed is not empty"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"."),s("length"),n("span",{class:"token operator"},"=="),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"array was empty"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"return"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token comment"},"//check if the scene has already been built"),s(`
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},"."),s("children"),n("span",{class:"token punctuation"},"."),s("length"),n("span",{class:"token operator"},">"),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"Scene already present"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"return"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token comment"},"// Create cubes at each position of the random walk "),s(`
+ `),n("span",{class:"token keyword"},"for"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"let"),s(" i"),n("span",{class:"token operator"},"="),n("span",{class:"token number"},"0"),n("span",{class:"token punctuation"},";"),s(" i"),n("span",{class:"token operator"},"<"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"."),s("length"),n("span",{class:"token punctuation"},";"),s(" i"),n("span",{class:"token operator"},"++"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" option "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"InstantiateOptions"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ option`),n("span",{class:"token punctuation"},"."),s("context "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("context"),n("span",{class:"token punctuation"},";"),s(`
+ option`),n("span",{class:"token punctuation"},"."),s("parent"),n("span",{class:"token operator"},"="),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("gameObject"),n("span",{class:"token punctuation"},";"),s(`
+ option`),n("span",{class:"token punctuation"},"."),s("position "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("seed"),n("span",{class:"token punctuation"},"["),s("i"),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"if"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("prefab"),n("span",{class:"token operator"},"!="),n("span",{class:"token keyword"},"null"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" cube "),n("span",{class:"token operator"},"="),s(" GameObject"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"instantiate"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("prefab"),n("span",{class:"token punctuation"},","),s(" option"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token keyword"},"as"),s(" GameObject"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token builtin"},"console"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"log"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"----------- Scene Built ---------"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),s(),n("span",{class:"token function"},"getRandomDirection"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token operator"},":"),s(" Vector3 "),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" x "),n("span",{class:"token operator"},"="),s(" Math"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"random"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"<"),s(),n("span",{class:"token number"},"0.5"),s(),n("span",{class:"token operator"},"?"),s(),n("span",{class:"token operator"},"-"),n("span",{class:"token number"},"1"),s(),n("span",{class:"token operator"},":"),s(),n("span",{class:"token number"},"1"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" y "),n("span",{class:"token operator"},"="),s(" Math"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"random"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"<"),s(),n("span",{class:"token number"},"0.5"),s(),n("span",{class:"token operator"},"?"),s(),n("span",{class:"token operator"},"-"),n("span",{class:"token number"},"1"),s(),n("span",{class:"token operator"},":"),s(),n("span",{class:"token number"},"1"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"const"),s(" z "),n("span",{class:"token operator"},"="),s(" Math"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"random"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"<"),s(),n("span",{class:"token number"},"0.5"),s(),n("span",{class:"token operator"},"?"),s(),n("span",{class:"token operator"},"-"),n("span",{class:"token number"},"1"),s(),n("span",{class:"token operator"},":"),s(),n("span",{class:"token number"},"1"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"return"),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Vector3"),n("span",{class:"token punctuation"},"("),s("x"),n("span",{class:"token punctuation"},","),s(" y"),n("span",{class:"token punctuation"},","),s(" z"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+`),n("span",{class:"token punctuation"},"}"),s(`
+`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),q=n("p",null,"The above script is placed on an object (any Transform) and will generate an array of unique Vector3 positions for a specified length (seedSize) after generateSeed() is called (In this case it is called from a button: generateButton).",-1),V=n("p",null,"Once generated it will send the array to the server and build the scene. The building process consist of instantiating the prefab at each Vector3 position of the seed (this.seed) array.",-1),D=n("p",null,"Any user joining the same room after a seed has been generated and sent, will receive the seed from the server and trigger the callback onDataReceived() which will cache the seed array, disable the button, and build the scene with the prefab, according to the seed.",-1),T=n("p",null,"This gives a way to generate a scene and communicate the seed of that scene, for each user to build locally.",-1),X=n("p",null,"This was the solution I chose which worked better than instantiating a complex scene (>400 objects) with instantiateSynced which would occasionally cause bugs.",-1);function W(B,A){const e=o("contribution-preview"),c=o("contributions-author");return p(),i("div",null,[a(c,{overviewLink:"/docs/community/contributions",name:"Web3Kev",url:"https://github.com/Web3Kev",profileImage:"https://avatars.githubusercontent.com/u/106066970?s=100&v=4",githubUrl:"https://github.com/Web3Kev"},{default:t(()=>[a(e,{title:"Vertical Move in VR using the right joystick (Quest)",pageUrl:"/docs/community/contributions/web3kev/vertical-move-in-vr-using-the-right-joystick-quest"},{default:t(()=>[k,r,d,b]),_:1}),a(e,{title:"Squeeze to Scale (Object or World) in VR",pageUrl:"/docs/community/contributions/web3kev/squeeze-to-scale-object-or-world-in-vr"},{default:t(()=>[m,v,w,h,y]),_:1}),a(e,{title:"Network instantiation of multiple objects",pageUrl:"/docs/community/contributions/web3kev/network-instantiation-of-multiple-objects"},{default:t(()=>[f,g,S,x,j,_,R,O,z,q,V,D,T,X]),_:1})]),_:1})])}const M=l(u,[["render",W],["__file","index.html.vue"]]);export{M as default};
diff --git a/assets/index.html-d8d34b98.js b/assets/index.html-d8d34b98.js
new file mode 100644
index 000000000..18fb71569
--- /dev/null
+++ b/assets/index.html-d8d34b98.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-acde0924","path":"/community/contributions/marwie/usdz-hide-object-on-start/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/marwie: usdz hide object on start.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-dda76fbd.js b/assets/index.html-dda76fbd.js
new file mode 100644
index 000000000..b084c2247
--- /dev/null
+++ b/assets/index.html-dda76fbd.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-ea622532","path":"/community/contributions/robyer1/ar-move-scale-rotate-controls-for-needle-on-mobile/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/robyer1: ar move scale rotate controls for needle on mobile.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-f2afcbd3.js b/assets/index.html-f2afcbd3.js
new file mode 100644
index 000000000..330088a8c
--- /dev/null
+++ b/assets/index.html-f2afcbd3.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-5d69cca2","path":"/community/contributions/marwie/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/contributions: marwie.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/index.html-f5f757f2.js b/assets/index.html-f5f757f2.js
new file mode 100644
index 000000000..e29ac1bca
--- /dev/null
+++ b/assets/index.html-f5f757f2.js
@@ -0,0 +1,56 @@
+import{_ as a,M as t,p as e,q as p,N as o,R as n,a1 as c}from"./framework-c782e227.js";const i={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=c(`
Use the mouse wheel or touch delta to update a timeline's time.
import{ Behaviour, PlayableDirector, serializeable }from"@needle-tools/engine";
+import{ Mathf }from"@needle-tools/engine";
+
+// Example of setting a timeline's time
+// without relying on any HTML elements.
+// Here we directly use the mousewheel scroll and the touch delta
+
+exportclassScrollTimeline_2extendsBehaviour{
+
+ @serializeable(PlayableDirector)
+ timeline?: PlayableDirector;
+
+ @serializeable()
+ scrollSpeed:number=0.5;
+
+ @serializeable()
+ lerpSpeed:number=2.5;
+
+ private targetTime:number=0;
+
+ start(){
+
+ this.timeline?.pause();
+
+ // Grab the mousewheel event
+ window.addEventListener("wheel",(evt: WheelEvent)=>this.updateTime(evt.deltaY));
+
+ // Touch events are a bit more complicated
+ // We need to keep track of the last touch position
+ // and calculate the delta between the current and the last position
+ let lastTouchPosition =-1;
+ window.addEventListener("touchmove",(evt: TouchEvent)=>{
+ const delta = evt.touches[0].clientY - lastTouchPosition;
+ // We only want to apply the delta if it's not TOO big
+ // e.g. when the user is scrolling the page
+ if(delta <10)this.updateTime(-delta);
+ // Update the last touch position
+ lastTouchPosition = evt.touches[0].clientY;
+ });
+ }
+
+ privateupdateTime(delta){
+ if(!this.timeline)return;
+ this.targetTime += delta *0.01*this.scrollSpeed;
+ this.targetTime = Mathf.clamp(this.targetTime,0,this.timeline.duration);
+ }
+
+ onBeforeRender():void{
+ if(!this.timeline)return;
+ this.timeline.pause();
+ this.timeline.time = Mathf.lerp(this.timeline.time,this.targetTime,this.lerpSpeed *this.context.time.deltaTime);
+ this.timeline.evaluate();
+ }
+}
+
+
`,2);function r(k,d){const s=t("contribution-header");return e(),p("div",null,[l,o(s,{url:"https://github.com/marwie",author:"marwie",page:"/docs/community/contributions/marwie",profileImage:"https://avatars.githubusercontent.com/u/5083203?s=100&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/155",title:"Control a Timeline by scroll",gradient:"True"}),u])}const v=a(i,[["render",r],["__file","index.html.vue"]]);export{v as default};
diff --git a/assets/index.html-f975bb82.js b/assets/index.html-f975bb82.js
new file mode 100644
index 000000000..db1f9e7ff
--- /dev/null
+++ b/assets/index.html-f975bb82.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-0c347631","path":"/community/contributions/marwie/code-contribution-example/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/marwie: code contribution example.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[],"git":{},"filePathRelative":null}');export{e as data};
diff --git a/assets/ktx-env-variable-d006aea1.js b/assets/ktx-env-variable-d006aea1.js
new file mode 100644
index 000000000..c6b04a827
--- /dev/null
+++ b/assets/ktx-env-variable-d006aea1.js
@@ -0,0 +1 @@
+const s="/docs/imgs/ktx-env-variable.webp";export{s as _};
diff --git a/assets/logo-b695892f.js b/assets/logo-b695892f.js
new file mode 100644
index 000000000..d1b892766
--- /dev/null
+++ b/assets/logo-b695892f.js
@@ -0,0 +1 @@
+const o="/docs/blender/logo.png";export{o as _};
diff --git a/assets/meta-test.html-3d11a75c.js b/assets/meta-test.html-3d11a75c.js
new file mode 100644
index 000000000..eb34cf8c3
--- /dev/null
+++ b/assets/meta-test.html-3d11a75c.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-0ca6c8f8","path":"/meta-test.html","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/meta test.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[{"level":3,"title":"my_test_header","slug":"my-test-header","link":"#my-test-header","children":[]},{"level":3,"title":"UI Canvas","slug":"ui-canvas","link":"#ui-canvas","children":[]},{"level":3,"title":"How physics works","slug":"how-physics-works","link":"#how-physics-works","children":[]}],"git":{},"filePathRelative":"_meta-test.md"}');export{e as data};
diff --git a/assets/meta-test.html-90f0fa30.js b/assets/meta-test.html-90f0fa30.js
new file mode 100644
index 000000000..4dcfc0188
--- /dev/null
+++ b/assets/meta-test.html-90f0fa30.js
@@ -0,0 +1,3 @@
+import{_ as t,M as o,p as r,q as n,R as a,t as s,N as i,a1 as h}from"./framework-c782e227.js";const d={},m=h(`
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sy but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looIt uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo
There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage
of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo
but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo
`,8);function u(l,f){const e=o("metalink");return r(),n("div",null,[a("p",null,[s("Hello world "),i(e)]),m])}const g=t(d,[["render",u],["__file","meta-test.html.vue"]]);export{g as default};
diff --git a/assets/metalink-e894783b.js b/assets/metalink-e894783b.js
new file mode 100644
index 000000000..97030b6b7
--- /dev/null
+++ b/assets/metalink-e894783b.js
@@ -0,0 +1 @@
+import{_ as e}from"./framework-c782e227.js";const t={__name:"metalink",setup(_){return(a,n)=>" ``` hello ``` "}},o=e(t,[["__file","metalink.vue"]]);export{o as default};
diff --git a/assets/modules.html-1bfb4cc2.js b/assets/modules.html-1bfb4cc2.js
new file mode 100644
index 000000000..952a4bbf6
--- /dev/null
+++ b/assets/modules.html-1bfb4cc2.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-bb6009aa","path":"/modules.html","title":"Modules and Packages","lang":"en-US","frontmatter":{"title":"Modules and Packages","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/modules and packages.png"}],["meta",{"name":"og:description","content":"---\\nProjects can be composed of re-usable pieces that we call NpmDef (which stands for Npm Defintion File).\\nBelow you can find links to other repositories that contain Unity packages. These packages can be installed like any Unity package and used in your own projects. They usually contain eihter examples or modules that we use ourselves, but that are not ready to be part of the core Needle Engine."}]],"description":"---\\nProjects can be composed of re-usable pieces that we call NpmDef (which stands for Npm Defintion File).\\nBelow you can find links to other repositories that contain Unity packages. These packages can be installed like any Unity package and used in your own projects. They usually contain eihter examples or modules that we use ourselves, but that are not ready to be part of the core Needle Engine."},"headers":[],"git":{},"filePathRelative":"modules.md"}');export{e as data};
diff --git a/assets/modules.html-6db0a17d.js b/assets/modules.html-6db0a17d.js
new file mode 100644
index 000000000..03ddf411f
--- /dev/null
+++ b/assets/modules.html-6db0a17d.js
@@ -0,0 +1 @@
+import{_ as i,M as t,p as a,q as l,R as e,t as o,N as n,V as c,a1 as p}from"./framework-c782e227.js";const d={},u=e("strong",null,"NpmDef",-1),m=p("
Below you can find links to other repositories that contain Unity packages. These packages can be installed like any Unity package and used in your own projects. They usually contain eihter examples or modules that we use ourselves, but that are not ready to be part of the core Needle Engine.
Custom Timeline Tracks Video Track (sync video playback to a Timeline) CssTrack (control css properties from the Timeline)
Splines (for Unity 2022.1+) Export splines to three.js SplineWalker for controlling camera motion based on a spline Timeline track to control SplineWalker
Google Drive Integration Sketches around drag-drop integration of Google Drive, file picking, app integration
",2),h={href:"https://github.com/needle-tools/needle-engine-modules",target:"_blank",rel:"noopener noreferrer"};function _(f,g){const r=t("RouterLink"),s=t("ExternalLinkIcon");return a(),l("div",null,[e("p",null,[o("Projects can be composed of re-usable pieces that we call "),n(r,{to:"/project-structure.html#npm-definition-files"},{default:c(()=>[u]),_:1}),o(" (which stands for Npm Defintion File).")]),m,e("p",null,[e("a",h,[o("Github repository"),n(s)])])])}const b=i(d,[["render",_],["__file","modules.html.vue"]]);export{b as default};
diff --git a/assets/needle-button-5931f2d1.js b/assets/needle-button-5931f2d1.js
new file mode 100644
index 000000000..ada0fe0c8
--- /dev/null
+++ b/assets/needle-button-5931f2d1.js
@@ -0,0 +1 @@
+import{_,p as r,q as d,R as l,s as b,ad as c}from"./framework-c782e227.js";const t={props:{href:String,secondary:Boolean,same_tab:Boolean,large:Boolean}},n=()=>{c(e=>({bdbb8492:e.secondary?"#aaa":"#826bed","8142adbe":e.large?"1em":".8em",54718738:e.secondary?"#bbb":"#6248be"}))},o=t.setup;t.setup=o?(e,a)=>(n(),o(e,a)):n;const u=["href","target"];function f(e,a,s,p,i,m){return r(),d("a",{href:s.href,target:s.same_tab?"_self":"_blank"},[l("button",null,[b(e.$slots,"default",{},void 0,!0)])],8,u)}const g=_(t,[["render",f],["__scopeId","data-v-475bbd8b"],["__file","needle-button.vue"]]);export{g as default};
diff --git a/assets/needle-config-json.html-182fd502.js b/assets/needle-config-json.html-182fd502.js
new file mode 100644
index 000000000..dde2c5cda
--- /dev/null
+++ b/assets/needle-config-json.html-182fd502.js
@@ -0,0 +1,25 @@
+import{_ as t,M as n,p as a,q as o,R as s,N as r,V as i,t as c,a1 as p}from"./framework-c782e227.js";const d={},l=p(`
The needle.config.json is used to provide configuration for the Needle Editor integrations and for the Needle Engine build pipeline plugins.
Paths
buildDirectory
This is where the built project files are being copied to
assetsDirectory
This is where the Editor integration assets will be copied to or created at (e.g. the .glb files exported from Unity or Blender)
scriptsDirectory
This is the directory the Editor integration is watching for code changes to re-generate components
codegenDirectory
This is where the Editor integration is outputting generated files to.
baseUrl
Required for e.g. next.js or SvelteKit integration. When baseUrl is set, relative paths for codegen and inside files are using baseUrl, not assetsDirectory. This is useful in cases where the assetDirectory does not match the server url. For example, the path on disk could be "assetsDirectory": "public/assets", but the framework serves files from "baseUrl": "assets".
Tools
build : { copy: ["myFileOrDirectory"] }
Array of string paths for copying additional files or folders to the buildDirectory. These can either be absolute or relative.
# Example with different baseUrl (e.g. SvelteKit, Next.js)
Files are exported to static/assets but the framework serves them from /assets. In this case, the baseUrl needs to be set to assets so that relative paths in files are correct.
`,10);function u(h,k){const e=n("RouterLink");return a(),o("div",null,[l,s("ul",null,[s("li",null,[r(e,{to:"/project-structure.html"},{default:i(()=>[c("Project Structure")]),_:1})])])])}const q=t(d,[["render",u],["__file","needle-config-json.html.vue"]]);export{q as default};
diff --git a/assets/needle-config-json.html-f4e643bb.js b/assets/needle-config-json.html-f4e643bb.js
new file mode 100644
index 000000000..c5854e21d
--- /dev/null
+++ b/assets/needle-config-json.html-f4e643bb.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-f7b8f1f2","path":"/reference/needle-config-json.html","title":"Needle Config","lang":"en-US","frontmatter":{"title":"Needle Config","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/needle config.png"}],["meta",{"name":"og:description","content":"---\\nThe needle.config.json is used to provide configuration for the Needle Editor integrations and for the Needle Engine build pipeline plugins."}]],"description":"---\\nThe needle.config.json is used to provide configuration for the Needle Editor integrations and for the Needle Engine build pipeline plugins."},"headers":[],"git":{},"filePathRelative":"reference/needle-config-json.md"}');export{e as data};
diff --git a/assets/needle-engine-attributes.html-03838d3f.js b/assets/needle-engine-attributes.html-03838d3f.js
new file mode 100644
index 000000000..0c7675ddd
--- /dev/null
+++ b/assets/needle-engine-attributes.html-03838d3f.js
@@ -0,0 +1,20 @@
+import{_ as l}from"./custom-loading-style-65a23c0d.js";import{_ as o,M as c,p as d,q as i,R as n,t,N as a,a1 as s}from"./framework-c782e227.js";const u={},p=n("p",null,[t("The "),n("code",null,""),t(" web-component comes with a nice collection of built-in attributes that can be used to modify the look and feel of the loaded scene without the need to add or edit the three.js scene directly."),n("br"),t(" The table below shows a list of the most important ones:")],-1),r=n("thead",null,[n("tr",null,[n("th",null,"Attribute"),n("th",null,"Description")])],-1),g=n("tr",null,[n("td",null,[n("strong",null,"Loading")]),n("td")],-1),h=n("tr",null,[n("td",null,[n("code",null,"src")]),n("td",null,[t("Path to one or multiple glTF or glb files."),n("br"),t("Supported types are "),n("code",null,"string"),t(", "),n("code",null,"string[]"),t(" or a stringified array ("),n("code",null,","),t(" separated)")])],-1),m=n("tr",null,[n("td",null,[n("code",null,"dracoDecoderPath")]),n("td",null,"URL to the draco decoder")],-1),k=n("td",null,[n("code",null,"dracoDecoderType")],-1),_=n("code",null,"wasm",-1),v=n("code",null,"js",-1),b={href:"https://threejs.org/docs/#examples/en/loaders/DRACOLoader.setDecoderConfig",target:"_blank",rel:"noopener noreferrer"},f=n("tr",null,[n("td",null,[n("code",null,"ktx2DecoderPath")]),n("td",null,"URL to the KTX2 decoder")],-1),y=n("tr",null,[n("td",null,[n("strong",null,"Rendering")]),n("td")],-1),x=n("tr",null,[n("td",null,[n("code",null,"skybox-image")]),n("td",null,"URL to a skybox image (background image)")],-1),q=n("tr",null,[n("td",null,[n("code",null,"environment-image")]),n("td",null,"optional, URL to a environment image (environment light)")],-1),w=n("tr",null,[n("td",null,[n("strong",null,"Interaction")]),n("td")],-1),R=n("tr",null,[n("td",null,[n("code",null,"autoplay")]),n("td",null,[t("add or set to "),n("code",null,"true"),t(" to auto play animations e.g. "),n("code",null," 3.17.1")])],-1),H=n("tr",null,[n("td",null,[n("strong",null,"Internal")]),n("td")],-1),V=n("tr",null,[n("td",null,[n("code",null,"hash")]),n("td",null,"Used internally, is appended to the files being loaded to force an update (e.g. when the browser has cached a glb file). Should not be edited manually.")],-1),B=s(`
You can easily modify how Needle Engine looks by setting the appropriate attributes on the <needle-engine> web component. Please see the table above for details.
`,5),M=n("img",{src:l,alt:"custom loading"},null,-1),Y=n("br",null,null,-1),G={href:"https://github.com/needle-engine/vite-template/blob/loading-style/custom/index.html",target:"_blank",rel:"noopener noreferrer"};function X(J,Q){const e=c("ExternalLinkIcon");return d(),i("div",null,[p,n("table",null,[r,n("tbody",null,[g,h,m,n("tr",null,[k,n("td",null,[t("draco decoder type. Options are "),_,t(" or "),v,t(". See "),n("a",b,[t("three.js documentation"),a(e)])])]),f,y,x,q,w,R,D,P,C,N,E,L,O,S,T,I,U,j,F,A,H,V])]),B,n("p",null,[t("Setting environment images, playing animation and automatic camera controls. "),n("a",z,[t("See it live on stackblitz"),a(e)])]),K,n("p",null,[M,Y,n("a",G,[t("See code on github"),a(e)])])])}const $=o(u,[["render",X],["__file","needle-engine-attributes.html.vue"]]);export{$ as default};
diff --git a/assets/needle-engine-attributes.html-37d7bf32.js b/assets/needle-engine-attributes.html-37d7bf32.js
new file mode 100644
index 000000000..98a2a31d2
--- /dev/null
+++ b/assets/needle-engine-attributes.html-37d7bf32.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-caed7310","path":"/reference/needle-engine-attributes.html","title":"Needle Engine Attributes","lang":"en-US","frontmatter":{"title":"Needle Engine Attributes","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/needle engine attributes.png"}],["meta",{"name":"og:description","content":"---\\nThe"}]],"description":"---\\nThe"},"headers":[{"level":3,"title":"Custom Loading Style (PRO)","slug":"custom-loading-style-pro","link":"#custom-loading-style-pro","children":[]}],"git":{},"filePathRelative":"reference/needle-engine-attributes.md"}');export{e as data};
diff --git a/assets/networking.html-0564acf1.js b/assets/networking.html-0564acf1.js
new file mode 100644
index 000000000..9cc174c5b
--- /dev/null
+++ b/assets/networking.html-0564acf1.js
@@ -0,0 +1,65 @@
+import{_ as o,M as i,p as c,q as p,R as n,t as e,N as a,a1 as t}from"./framework-c782e227.js";const r="/docs/imgs/networking_absolute.webp",l={},d=n("h1",{id:"networking",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#networking","aria-hidden":"true"},"#"),e(" Networking")],-1),u=n("p",null,[e("Access to core networking functionality can be obtained by using "),n("code",null,"this.context.connection"),e(" from a component. The default backend server connects users to rooms. Users in the same room will share state and receive messages from each other.")],-1),k={href:"https://github.com/jjxxs/websocket-ts",target:"_blank",rel:"noopener noreferrer"},h={href:"https://google.github.io/flatbuffers/",target:"_blank",rel:"noopener noreferrer"},m=t(`
SyncedRoom โ handles networking connection and connection to a room. This can also be done by code using the networking api accessible from this.context.connection
When sending an object containing a guid field it will be saved in the persistant storage and automatically sent to users that connect later or come back later to the site (e.g. to restore state). To delete state for a specific guid from the backend storage you can use delete-state as the key and provide an object with { guid: "guid_to_delete" }
Subscribe to json events / listen to events in the room using a specific key this.context.connection.beginListen(key:string, callback:(data) => void) Unsubscribe with stopListening
Subscribe to flatbuffer binary events this.context.connection.beginListenBinrary(identifier:string, callback:(data : ByteBuffer) => void) Unsubscribe with stopListenBinary
To automatically network fields in a component you can just decorate a field with a @syncField() decorator (note: you need to have experimentalDecorators: true in your tsconfig.json file for it to work)
Example Code
Automatically network a color field. The following script also changes the color randomly on click
import{ Behaviour, IPointerClickHandler, PointerEventData, Renderer, RoomEvents, delay, serializable, showBalloonMessage, syncField }from"@needle-tools/engine";
+import{ Color }from"three"
+
+exportclassNetworking_ClickToChangeColorextendsBehaviourimplementsIPointerClickHandler{
+
+ // START MARKER network color change syncField
+ /** syncField does automatically send a property value when it changes */
+ @syncField(Networking_ClickToChangeColor.prototype.onColorChanged)
+ @serializable(Color)
+ color!: Color;
+
+ privateonColorChanged(){
+ // syncField will network the color as a number, so we need to convert it back to a Color when we receive it
+ if(typeofthis.color ==="number")
+ this.color =newColor(this.color);
+ this.setColorToMaterials();
+ }
+ // END MARKER network color change syncField
+
+ /** called when the object is clicked and does generate a random color */
+ onPointerClick(_: PointerEventData){
+ const randomColor =newColor(Math.random(), Math.random(), Math.random());
+ this.color = randomColor;
+ }
+
+ onEnable(){
+ this.setColorToMaterials();
+ }
+
+ privatesetColorToMaterials(){
+ const renderer =this.gameObject.getComponent(Renderer);
+ if(renderer){
+ for(let i =0; i < renderer.sharedMaterials.length; i++){
+ // we clone the material so that we don't change the original material
+ // just for demonstration purposes, you can also change the original material
+ const mat = renderer.sharedMaterials[i]?.clone();
+ renderer.sharedMaterials[i]= mat;
+ if(mat &&"color"in mat)
+ mat.color =this.color;
+ }
+ }
+ elseconsole.warn("No renderer found",this.gameObject)
+ }
+
+}
+
Simple networking of a number
import{ Behaviour, syncField }from"@needle-tools/engine"
+
+exportclassAutoFieldSyncextendsBehaviourimplementsIPointerClickHandler{
+
+ // Use \`@syncField\` to automatically network a field.
+ // You can optionally assign a method or method name to be called when the value changes
+ @syncField("myValueChanged")
+ mySyncedValue?:number=1;
+
+ privatemyValueChanged(){
+ console.log("My value changed",this.mySyncedValue);
+ }
+
+ onPointerClick(){
+ this.mySyncedValue = Math.random();
+ }
+}
+
It can be added to your own fastiy or express server running on any server for example by adding the following code on your server after installing the package:
When deploying your app to Glitch, we include a simple networking backend that is great for prototyping and small deployments (~15-20 people at the same time). You can later update to a bigger/better/stronger networking solution if required.
For testing and development purposes it can be desired to run the needle engine networking package on a local server. We have prepared a repository that is setup to host the websocket package and to make that easy for you. Please follow the instructions in the linked repository:
`,10),x={href:"https://fwd.needle.tools/needle-engine/local-networking-repository",target:"_blank",rel:"noopener noreferrer"},C=n("h2",{id:"hosting-your-own-networking-server",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#hosting-your-own-networking-server","aria-hidden":"true"},"#"),e(" Hosting your own Networking Server")],-1),R=n("strong",null,"google cloud",-1),j={href:"https://fwd.needle.tools/needle-engine/local-networking-repository",target:"_blank",rel:"noopener noreferrer"},S=n("p",null,[e("If you want to use a different server for your local development and your hosted development (and the hosted server is not the same as your needle engine deployed website) then you can also enter a absolute URL in the "),n("code",null,"Networking"),e(" component "),n("code",null,"URL"),e(" field as well:")],-1),N=n("p",null,[n("img",{src:r,alt:"Needle Engine Networking component with networking server hosted elswhere"})],-1),q=n("h2",{id:"peerjs",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#peerjs","aria-hidden":"true"},"#"),e(" peerjs")],-1),E=n("code",null,"Screencapture",-1),L=n("code",null,"Voip",-1),T={href:"https://peerjs.com/",target:"_blank",rel:"noopener noreferrer"},M=n("h3",{id:"customizing-peerjs-options",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#customizing-peerjs-options","aria-hidden":"true"},"#"),e(" Customizing peerjs options")],-1),A=n("ul",null,[n("li",null,[e("If you want to modify the default peerjs options you can call "),n("code",null,"setPeerOptions(opts: PeerjsOptions)"),e(" with your custom options. This can be used to modify the hosting provider in case where you host your own peerjs server.")])],-1);function V(F,P){const s=i("ExternalLinkIcon");return c(),p("div",null,[d,u,n("p",null,[e("Networking is currently based on "),n("a",k,[e("websockets"),a(s)]),e(" and sending either json strings (for infrequent updates) or "),n("a",h,[e("flatbuffers"),a(s)]),e(" (for frequent updates). Continue reading below for more details:")]),m,n("ul",null,[n("li",null,[n("a",g,[e("Using the schema compiler"),a(s)])]),n("li",null,[n("a",b,[e("Generating a schema"),a(s)])]),n("li",null,[n("a",v,[e("Flatbuffer in Typescript"),a(s)])])]),f,n("p",null,[e("Needle Engine currently uses its "),n("a",w,[e("own networking package"),a(s)]),e(" hosted on npm. By default if not configured differently using the "),y,e(" component Needle Engine will connect to a server running on Glitch.")]),_,n("ul",null,[n("li",null,[n("a",x,[e("Local Networking Repository"),a(s)])])]),C,n("p",null,[e("You can also deploy your own networking server on e.g. "),R,e(". For further instructions please refer to the description found here: "),n("a",j,[e("Local Networking Repository"),a(s)])]),S,N,q,n("p",null,[e("Needle Engine "),E,e(" / Screensharing and "),L,e(" components use "),n("a",T,[e("peerjs"),a(s)]),e(" for networking audio and video.")]),M,A])}const U=o(l,[["render",V],["__file","networking.html.vue"]]);export{U as default};
diff --git a/assets/networking.html-bca0a9b0.js b/assets/networking.html-bca0a9b0.js
new file mode 100644
index 000000000..f19389656
--- /dev/null
+++ b/assets/networking.html-bca0a9b0.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-dda7a368","path":"/networking.html","title":"Networking","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/networking.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[{"level":2,"title":"Using Multiplayer","slug":"using-multiplayer","link":"#using-multiplayer","children":[]},{"level":2,"title":"Core Components","slug":"core-components","link":"#core-components","children":[]},{"level":2,"title":"Manual Networking","slug":"manual-networking","link":"#manual-networking","children":[{"level":3,"title":"Sending","slug":"sending","link":"#sending","children":[]},{"level":3,"title":"Receiving","slug":"receiving","link":"#receiving","children":[]}]},{"level":2,"title":"Auto Networking (experimental)","slug":"auto-networking-experimental","link":"#auto-networking-experimental","children":[]},{"level":2,"title":"Flatbuffers for your own components","slug":"flatbuffers-for-your-own-components","link":"#flatbuffers-for-your-own-components","children":[]},{"level":2,"title":"Networking Package","slug":"networking-package","link":"#networking-package","children":[]},{"level":2,"title":"Networking on Glitch","slug":"networking-on-glitch","link":"#networking-on-glitch","children":[{"level":3,"title":"Limitations","slug":"limitations","link":"#limitations","children":[]}]},{"level":2,"title":"Local Networking","slug":"local-networking","link":"#local-networking","children":[]},{"level":2,"title":"Hosting your own Networking Server","slug":"hosting-your-own-networking-server","link":"#hosting-your-own-networking-server","children":[]},{"level":2,"title":"peerjs","slug":"peerjs","link":"#peerjs","children":[{"level":3,"title":"Customizing peerjs options","slug":"customizing-peerjs-options","link":"#customizing-peerjs-options","children":[]}]}],"git":{},"filePathRelative":"networking.md"}');export{e as data};
diff --git a/assets/os-link-d1f0564f.js b/assets/os-link-d1f0564f.js
new file mode 100644
index 000000000..6d1892597
--- /dev/null
+++ b/assets/os-link-d1f0564f.js
@@ -0,0 +1 @@
+import{_ as s,p as e,q as n,s as o,t as l,v as u}from"./framework-c782e227.js";const _={props:{text:String,generic_url:String,windows_url:String,osx_url:String,osx_silicon_url:String,linux_url:String},methods:{getUrl:function(){const r=navigator.userAgent;if(r.indexOf("Windows")!==-1){if(this.windows_url)return this.windows_url}else if(r.indexOf("Mac")!==-1){if(this.osx_silicon_url&&r.indexOf("Intel")===-1)return this.osx_silicon_url;if(this.osx_url)return this.osx_url}else if(r.indexOf("Linux")!==-1){if(this.linux_url)return this.linux_url;if(this.osx_url)return this.osx_url}return this.generic_url??this.windows_url}}},f=["href"];function c(r,x,i,a,d,t){return e(),n("a",{href:t.getUrl()},[o(r.$slots,"default",{},()=>[l(u(i.text),1)])],8,f)}const g=s(_,[["render",c],["__file","os-link.vue"]]);export{g as default};
diff --git a/assets/project-structure.html-1ea19a65.js b/assets/project-structure.html-1ea19a65.js
new file mode 100644
index 000000000..86ba49ab3
--- /dev/null
+++ b/assets/project-structure.html-1ea19a65.js
@@ -0,0 +1 @@
+import{_ as a,M as s,p as d,q as c,R as e,N as n,V as l,t,a1 as r}from"./framework-c782e227.js";const u="/docs/imgs/unity-project-local-template.jpg",h="/docs/imgs/unity-project-remote-template.jpg",p={},g=e("h1",{id:"needle-engine-project-structure",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#needle-engine-project-structure","aria-hidden":"true"},"#"),t(" Needle Engine Project Structure")],-1),f=e("h3",{id:"web-project-files",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#web-project-files","aria-hidden":"true"},"#"),t(" Web Project Files")],-1),_=e("thead",null,[e("tr",null,[e("th"),e("th")])],-1),m=e("tr",null,[e("td",null,[e("strong",null,"Needle Engine")]),e("td")],-1),y=e("code",null,"needle.config.json",-1),b=e("td",null,"Configuration for Needle Engine builds and integrations",-1),j=e("tr",null,[e("td",null,[e("strong",null,"Ecosystem")]),e("td")],-1),w=e("tr",null,[e("td",null,[e("code",null,"package.json")]),e("td",null,"Project configuration containing name, version, dependencies and scripts")],-1),k=e("tr",null,[e("td",null,[e("code",null,"tsconfig.json")]),e("td",null,"Typescript compiler configuration")],-1),v=e("tr",null,[e("td",null,[e("code",null,".gitignore")]),e("td",null,"Files and folders to be ignored in git")],-1),x=e("tr",null,[e("td",null,[e("code",null,"vite.config.js")]),e("td",null,[t("Contains vite specific configuration."),e("br"),t("It also adds the Needle Engine vite plugins.")])],-1),E=e("h3",{id:"default-vite-project-structure",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#default-vite-project-structure","aria-hidden":"true"},"#"),t(" Default Vite project structure")],-1),P={href:"https://vitejs.dev/",target:"_blank",rel:"noopener noreferrer"},N=e("thead",null,[e("tr",null,[e("th"),e("th")])],-1),T=e("tr",null,[e("td",null,[e("strong",null,"Folders")]),e("td")],-1),U=e("tr",null,[e("td",null,[e("code",null,"assets/")]),e("td",null,[t("The asset folder contains exported assets from Unity. E.g. generated "),e("code",null,"gltf"),t(" files, audio or video files. It is not recommended to manually add files to "),e("code",null,"assets"),t(" as it will get cleared on building the distribution for the project.")])],-1),I=e("tr",null,[e("td",null,[e("code",null,"include/")]),e("td",null,"(optional) - If you have custom assets that you need to reference/load add them to the include directory. On build this directory will be copied to the output folder.")],-1),C=e("tr",null,[e("td",null,[e("code",null,"src/generated/")]),e("td",null,"The generated javascript code. Do not edit manually!")],-1),D=e("tr",null,[e("td",null,[e("code",null,"src/scripts/")]),e("td",null,"Your project specific scripts / components")],-1),L=e("tr",null,[e("td",null,[e("code",null,"src/styles/")]),e("td",null,"Stylesheets")],-1),M=e("td",null,[e("code",null,"*")],-1),F=e("tr",null,[e("td",null,[e("strong",null,"Files")]),e("td")],-1),V=e("tr",null,[e("td",null,[e("code",null,"index.html")]),e("td",null,"The landing- or homepage of your website")],-1),S=e("td",null,[e("code",null,"vite.config")],-1),R={href:"https://vitejs.dev/config/",target:"_blank",rel:"noopener noreferrer"},W=e("tr",null,[e("td",null,[e("code",null,"src/main.ts")]),e("td",null,[t("Included from "),e("code",null,"index.html"),t(" and importing "),e("code",null,"needle-engine")])],-1),Y=e("td",null,[e("code",null,"*")],-1),q=e("p",null,"Our exporter can be used with other project structures as well, vite is just our go-to frontend bundling tool because of its speed. Feel free to set up your JavaScript project as you like.",-1),A=e("hr",null,null,-1),B=e("h1",{id:"unity-project-folders-and-files",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#unity-project-folders-and-files","aria-hidden":"true"},"#"),t(" Unity Project Folders and Files")],-1),O=e("thead",null,[e("tr",null,[e("th",null,"Folder"),e("th")])],-1),G=e("tr",null,[e("td",null,[e("strong",null,"Unity")]),e("td")],-1),J=e("tr",null,[e("td",null,[e("code",null,"Assets")]),e("td",null,"This is where project specific/exclusive assets live.")],-1),z=e("td",null,[e("code",null,"Packages")],-1),H={href:"https://docs.unity3d.com/Manual/PackagesList.html",target:"_blank",rel:"noopener noreferrer"},K=e("tr",null,[e("td",null,[e("strong",null,"Needle Engine Unity Package")]),e("td")],-1),Q=e("tr",null,[e("td",null,[e("code",null,"Core/Runtime/Components")]),e("td",null,[t("Contains all Needle Engine built-in runtime components (See "),e("code",null,"Packages/Needle Engine Exporter"),t(" in the Unity Project Window)")])],-1),X=e("h2",{id:"projects-in-unity",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#projects-in-unity","aria-hidden":"true"},"#"),t(" Projects in Unity")],-1),Z=e("p",null,[t("When creating a new web project in Unity you can choose to create it from a local template (by default we ship a vite based web template). It is also possible to create custom local templates using the Project View context menu in "),e("code",null,"Create/Needle Engine/Project Template"),t(".")],-1),$=e("p",null,[e("img",{src:u,alt:"Unity ExportInfo local templates"})],-1),ee=e("code",null,"needle.config.json",-1),te={href:"https://github.com/needle-engine",target:"_blank",rel:"noopener noreferrer"},ne=r('
If you're planning to only add custom files via NpmDefs and not change the project config (e.g. for a quick fullscreen test), you can prefix the project path with Library. The project will be generated in the Unity Project Library and does not need to be added to source control (the Library folder should be excluded from source control). We call these projects temporary projects. They're great for quickly testing out ideas!
To create a NPM Definition right click in the Unity Project browser and select Create/NPM Definition. You can install a NPM Definition package to your runtime project by e.g. selecting your Export Info component and adding it to the dependencies list (internally this will just add the underlying npm package to your package.json).
Don't forget to install the newly added package by e.g. clicking Install on the ExportInfo component and also restart the server if it is already running
To edit the code inside a NPM Definition package just double click the asset NPM Definition asset in your project browser and it will open the vscode workspace that comes with each npmdef.
',7);function se(re,ae){const o=s("RouterLink"),i=s("ExternalLinkIcon");return d(),c("div",null,[g,f,e("table",null,[_,e("tbody",null,[m,e("tr",null,[e("td",null,[n(o,{to:"/reference/needle-config-json.html"},{default:l(()=>[y]),_:1})]),b]),j,w,k,v,x])]),E,e("p",null,[t("Our main project template uses the superfast "),e("a",P,[t("vite"),n(i)]),t(" bundler. The following shows the structure of the Vite template that we created and ship (altough it is possible to adapt it to your own needs).")]),e("table",null,[N,e("tbody",null,[T,U,I,C,D,L,e("tr",null,[M,e("td",null,[t("You can add any new folders here as you please. Make sure to "),n(o,{to:"/reference/needle-config-json.html"},{default:l(()=>[t("copy")]),_:1}),t(" them to the output directory when building")])]),F,V,e("tr",null,[S,e("td",null,[t("The "),e("a",R,[t("vite config"),n(i)]),t(". Settings for building the distribution and hosting the development server are made here. It is usually not necessary to edit these settings.")])]),W,e("tr",null,[Y,e("td",null,[t("You can add any new files here as you please. Make sure to "),n(o,{to:"/reference/needle-config-json.html"},{default:l(()=>[t("copy")]),_:1}),t(" them to the output directory when building (unless they are just being used during development)")])])])]),q,e("p",null,[n(o,{to:"/html.html"},{default:l(()=>[t("Learn more in the docs about bundling and usage with other frameworks")]),_:1})]),A,B,e("table",null,[O,e("tbody",null,[G,J,e("tr",null,[z,e("td",null,[t("This is where packages installed for this project live. A package can contain any asset type. The main difference is that it can be added to multiple Unity projects. It therefor is a great method to share code or assets. To learn more about packages see "),e("a",H,[t("the Unity documentation about packages"),n(i)]),t(".")])]),K,Q])]),X,Z,$,e("p",null,[t("You can also reference remote templates by entering a repository URL in the ExportInfo project path (this can be saved with your scene for example). When creating a new web project the repository will be either cloned or downloaded (depending on if you have git installed) and searched for a "),ee,t(" file. If none can be found in the cloned repository the root directory will be used. Examples of remote template projects can be found on "),e("a",te,[t("github.com/needle-engine"),n(i)])]),ne,e("p",null,[oe,t(" are "),e("a",le,[t("npm packages"),n(i)]),t(" tightly integrated into the Unity Editor which makes it easily possible to share scripts with multiple web- or even Unity projects.")]),ie,e("ul",null,[e("li",null,[n(o,{to:"/getting-started/for-unity-developers.html"},{default:l(()=>[t("Typescript Guide for Unity Developers")]),_:1})]),e("li",null,[n(o,{to:"/getting-started/typescript-essentials.html"},{default:l(()=>[t("Typescript Essentials")]),_:1})]),e("li",null,[n(o,{to:"/scripting.html"},{default:l(()=>[t("Writing custom scripts")]),_:1})]),e("li",null,[n(o,{to:"/everywhere-actions.html"},{default:l(()=>[t("Everywhere Actions")]),_:1})])])])}const ce=a(p,[["render",se],["__file","project-structure.html.vue"]]);export{ce as default};
diff --git a/assets/project-structure.html-fccb2f6e.js b/assets/project-structure.html-fccb2f6e.js
new file mode 100644
index 000000000..e57262aab
--- /dev/null
+++ b/assets/project-structure.html-fccb2f6e.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-5c95b2b3","path":"/project-structure.html","title":"Project Structure","lang":"en-US","frontmatter":{"title":"Project Structure","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/preview.jpeg"}],["meta",{"name":"og:description","content":"---"}]],"description":"---"},"headers":[{"level":3,"title":"Web Project Files","slug":"web-project-files","link":"#web-project-files","children":[]},{"level":3,"title":"Default Vite project structure","slug":"default-vite-project-structure","link":"#default-vite-project-structure","children":[]},{"level":2,"title":"Projects in Unity","slug":"projects-in-unity","link":"#projects-in-unity","children":[{"level":3,"title":"Temporary Projects","slug":"temporary-projects","link":"#temporary-projects","children":[]}]},{"level":2,"title":"Typescript in Unity","slug":"typescript-in-unity","link":"#typescript-in-unity","children":[]}],"git":{},"filePathRelative":"project-structure.md"}');export{e as data};
diff --git a/assets/quoteslides-e4e5699b.js b/assets/quoteslides-e4e5699b.js
new file mode 100644
index 000000000..47c9cec8a
--- /dev/null
+++ b/assets/quoteslides-e4e5699b.js
@@ -0,0 +1 @@
+import{d as n}from"./app-8ffcfbb5.js";import{_ as l,o as d,p as a,q as i,s as r}from"./framework-c782e227.js";const u=n({setup(){d(()=>{const o=document.querySelector(".quotes");o.style.display="flex";const e=document.querySelectorAll(".quotes > div");for(let s=0;s{e[t].style.display="none",t=(t+1)%e.length,e[t].style.display="block"},5e3)})}}),c={class:"quotes"};function _(o,e,t,s,p,f){return a(),i("div",c,[r(o.$slots,"default",{},void 0,!0)])}const m=l(u,[["render",_],["__scopeId","data-v-96af3692"],["__file","quoteslides.vue"]]);export{m as default};
diff --git a/assets/sample-6d4c3ac9.js b/assets/sample-6d4c3ac9.js
new file mode 100644
index 000000000..9942fed6b
--- /dev/null
+++ b/assets/sample-6d4c3ac9.js
@@ -0,0 +1 @@
+import{_ as c,p as a,q as s,R as l,Q as i}from"./framework-c782e227.js";const d={props:{src:String,split:{type:Boolean,default:!1},noRoom:{type:Boolean,default:!1}},data(){return{sanitizedUrl:""}},watch:{src:{immediate:!0,handler(n){const e=new URL(n),t=Math.random().toString(36).substring(2,6);e.searchParams.delete("room"),this.noRoom||e.searchParams.append("room",`needle_docs_${t}`),e.searchParams.append("hideClose",""),this.sanitizedUrl=e.toString()}}},mounted(){const n=e=>{if(!e||!e.contentWindow)return;const t=e.contentWindow.document.getElementsByTagName("iframe")[0];if(t){const r=t.contentWindow.document.querySelector("needle-engine");r&&(r.style.touchAction="pan-y")}};n(this.$refs.frame1),n(this.$refs.frame2)}},m=["src"],p=["src"];function f(n,e,t,o,r,u){return a(),s("div",null,[l("iframe",{src:r.sanitizedUrl,ref:"frame1",allow:"xr; xr-spatial-tracking; camera; microphone; fullscreen;display-capture"},null,8,m),t.split===!0?(a(),s("iframe",{key:0,src:r.sanitizedUrl,ref:"frame2",allow:"xr; xr-spatial-tracking; camera; microphone; fullscreen;display-capture"},null,8,p)):i("v-if",!0)])}const h=c(d,[["render",f],["__scopeId","data-v-916978fd"],["__file","sample.vue"]]);export{h as default};
diff --git a/assets/samples-and-modules.html-5d82466f.js b/assets/samples-and-modules.html-5d82466f.js
new file mode 100644
index 000000000..38bbf3417
--- /dev/null
+++ b/assets/samples-and-modules.html-5d82466f.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-761831b6","path":"/samples-and-modules.html","title":"Samples Projects","lang":"en-US","frontmatter":{"title":"Samples Projects","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/samples projects.png"}],["meta",{"name":"og:description","content":"---\\nView all samples at engine.needle.tools/samples with a live preview and links for download and installation.\\n
+
+
+
+
+
+
+
+
+
+ `),o(`
+Get the samples on github
+ `)])}const f=i(d,[["render",h],["__file","samples-and-modules.html.vue"]]);export{f as default};
diff --git a/assets/scripting-examples.html-d90e9d61.js b/assets/scripting-examples.html-d90e9d61.js
new file mode 100644
index 000000000..f02ec9a5b
--- /dev/null
+++ b/assets/scripting-examples.html-d90e9d61.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-ecef79fe","path":"/scripting-examples.html","title":"Script Examples","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/scripting examples.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[{"level":2,"title":"Basic component","slug":"basic-component","link":"#basic-component","children":[]},{"level":2,"title":"Reference an Object from Unity","slug":"reference-an-object-from-unity","link":"#reference-an-object-from-unity","children":[]},{"level":2,"title":"Reference and load an asset from Unity (Prefab or SceneAsset)","slug":"reference-and-load-an-asset-from-unity-prefab-or-sceneasset","link":"#reference-and-load-an-asset-from-unity-prefab-or-sceneasset","children":[]},{"level":2,"title":"Reference and load scenes from Unity","slug":"reference-and-load-scenes-from-unity","link":"#reference-and-load-scenes-from-unity","children":[]},{"level":2,"title":"Receive Clicks on Objects","slug":"receive-clicks-on-objects","link":"#receive-clicks-on-objects","children":[]},{"level":2,"title":"Networking Clicks on Objects","slug":"networking-clicks-on-objects","link":"#networking-clicks-on-objects","children":[{"level":3,"title":"Play Animation on click","slug":"play-animation-on-click","link":"#play-animation-on-click","children":[]}]},{"level":2,"title":"Reference an Animation Clip","slug":"reference-an-animation-clip","link":"#reference-an-animation-clip","children":[]},{"level":2,"title":"Create and invoke a UnityEvent","slug":"create-and-invoke-a-unityevent","link":"#create-and-invoke-a-unityevent","children":[{"level":3,"title":"Declare a custom event type","slug":"declare-a-custom-event-type","link":"#declare-a-custom-event-type","children":[]}]},{"level":2,"title":"Use nested objects and serialization","slug":"use-nested-objects-and-serialization","link":"#use-nested-objects-and-serialization","children":[]},{"level":2,"title":"Use Web APIs","slug":"use-web-apis","link":"#use-web-apis","children":[{"level":3,"title":"Display current location","slug":"display-current-location","link":"#display-current-location","children":[]},{"level":3,"title":"Display current time using a Coroutine","slug":"display-current-time-using-a-coroutine","link":"#display-current-time-using-a-coroutine","children":[]}]},{"level":2,"title":"Change custom shader property","slug":"change-custom-shader-property","link":"#change-custom-shader-property","children":[]},{"level":2,"title":"Switching src attribute","slug":"switching-src-attribute","link":"#switching-src-attribute","children":[]},{"level":2,"title":"Adding new postprocessing effects","slug":"adding-new-postprocessing-effects","link":"#adding-new-postprocessing-effects","children":[]},{"level":2,"title":"Custom ParticleSystem Behaviour","slug":"custom-particlesystem-behaviour","link":"#custom-particlesystem-behaviour","children":[]},{"level":2,"title":"Custom 2D Audio Component","slug":"custom-2d-audio-component","link":"#custom-2d-audio-component","children":[]},{"level":2,"title":"Arbitrary external files","slug":"arbitrary-external-files","link":"#arbitrary-external-files","children":[]},{"level":2,"title":"Receiving html element click in component","slug":"receiving-html-element-click-in-component","link":"#receiving-html-element-click-in-component","children":[]},{"level":2,"title":"Disable environment light","slug":"disable-environment-light","link":"#disable-environment-light","children":[]},{"level":2,"title":"Use mediapipe package to control the 3D scene with hands","slug":"use-mediapipe-package-to-control-the-3d-scene-with-hands","link":"#use-mediapipe-package-to-control-the-3d-scene-with-hands","children":[]},{"level":2,"title":"Change Color On Collision","slug":"change-color-on-collision","link":"#change-color-on-collision","children":[]},{"level":2,"title":"Physics Trigger Relay","slug":"physics-trigger-relay","link":"#physics-trigger-relay","children":[]},{"level":2,"title":"Auto Reset","slug":"auto-reset","link":"#auto-reset","children":[]},{"level":2,"title":"Play Audio On Collision","slug":"play-audio-on-collision","link":"#play-audio-on-collision","children":[]},{"level":2,"title":"Set Random Color","slug":"set-random-color","link":"#set-random-color","children":[]},{"level":2,"title":"Spawn Objects Over Time","slug":"spawn-objects-over-time","link":"#spawn-objects-over-time","children":[]}],"git":{},"filePathRelative":"scripting-examples.md"}');export{e as data};
diff --git a/assets/scripting-examples.html-f70a35aa.js b/assets/scripting-examples.html-f70a35aa.js
new file mode 100644
index 000000000..0e4e26f21
--- /dev/null
+++ b/assets/scripting-examples.html-f70a35aa.js
@@ -0,0 +1,749 @@
+import{_ as i,M as p,p as l,q as u,R as n,t as s,N as a,V as r,a1 as e}from"./framework-c782e227.js";const k={},d=n("h1",{id:"script-examples",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#script-examples","aria-hidden":"true"},"#"),s(" Script Examples")],-1),v=n("p",null,"Below you will find a few basic scripts as a quick reference. We also offer a lot of sample scenes and complete projects that you can download and use as a starting point:",-1),m={href:"https://engine.needle.tools/samples",target:"_blank",rel:"noopener noreferrer"},b={href:"https://engine.needle.tools/downloads/unity/samples",target:"_blank",rel:"noopener noreferrer"},h=n("h2",{id:"basic-component",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#basic-component","aria-hidden":"true"},"#"),s(" Basic component")],-1),y=e(`
import{ Behaviour, serializable }from"@needle-tools/engine";
+import{ Object3D }from"three"
+
+exportclassMyClassextendsBehaviour{
+ // this will be a "Transform" field in Unity
+ @serializable(Object3D)
+ myObjectReference: Object3D |null=null;
+
+ // this will be a "Transform" array field in Unity
+ // Note that the @serializable decorator contains the array content type! (Object3D and not Object3D[])
+ @serializable(Object3D)
+ myObjectReferenceList: Object3D[]|null=null;
+}
+
# Reference and load an asset from Unity (Prefab or SceneAsset)
import{ Behaviour, serializable, AssetReference }from"@needle-tools/engine";
+
+exportclassMyClassextendsBehaviour{
+
+ // if you export a prefab or scene as a reference from Unity you'll get a path to that asset
+ // which you can de-serialize to AssetReference for convenient loading
+ @serializable(AssetReference)
+ myPrefab?: AssetReference;
+
+ asyncstart(){
+ // directly instantiate
+ const myInstance =awaitthis.myPrefab?.instantiate();
+
+ // you can also just load and instantiate later
+ // const myInstance = await this.myPrefab.loadAssetAsync();
+ // this.gameObject.add(myInstance)
+ // this is useful if you know that you want to load this asset only once because it will not create a copy
+ // since \`\`instantiate()\`\` does create a copy of the asset after loading it
+ }
+}
+
import{ Behaviour, serializable, AssetReference }from"@needle-tools/engine";
+
+exportclassLoadingScenesextendsBehaviour{
+ // tell the component compiler that we want to reference an array of SceneAssets
+ // @type UnityEditor.SceneAsset[]
+ @serializable(AssetReference)
+ myScenes?: AssetReference[];
+
+ asyncawake(){
+ if(!this.myScenes){
+ return;
+ }
+ for(const scene ofthis.myScenes){
+ // check if it is assigned in unity
+ if(!scene)continue;
+ // load the scene once
+ const myScene =await scene.loadAssetAsync();
+ // add it to the threejs scene
+ this.gameObject.add(myScene);
+
+ // of course you can always just load one at a time
+ // and remove it from the scene when you want
+ // myScene.removeFromParent();
+ // this is the same as scene.asset.removeFromParent()
+ }
+ }
+
+ onDestroy():void{
+ if(!this.myScenes)return;
+ for(const scene ofthis.myScenes){
+ scene?.unload();
+ }
+ }
+}
+
Add this script to any object in your scene that you want to be clickable. Make sure to also have an ObjectRaycaster component in the parent hierarchy of that object.
`,3),q=e(`
import{ Behaviour, IPointerClickHandler, PointerEventData, showBalloonMessage }from"@needle-tools/engine";
+
+exportclassClickExampleextendsBehaviourimplementsIPointerClickHandler{
+
+ // Make sure to have an ObjectRaycaster component in the parent hierarchy
+ onPointerClick(_args: PointerEventData){
+ showBalloonMessage("Clicked "+this.name);
+ }
+}
+
Add this script to any object in your scene that you want to be clickable. Make sure to also have an ObjectRaycaster component in the parent hierarchy of that object. The component will send the received click to all connected clients and will raise an event that you can then react to in your app. If you are using Unity or Blender you can simply assign functions to call to the onClick event to e.g. play an animation or hide objects.
This is useful for when you want to expose an event to Unity or Blender with some custom arguments (like a string)
import{ Behaviour, serializable, EventList }from"@needle-tools/engine";
+import{ Object3D }from"three";
+
+/*
+Make sure to have a c# file in your project with the following content:
+
+using UnityEngine;
+using UnityEngine.Events;
+
+[System.Serializable]
+public class MyCustomUnityEvent : UnityEvent<string>
+{
+}
+
+Unity documentation about custom events:
+https://docs.unity3d.com/ScriptReference/Events.UnityEvent_2.html
+
+*/
+
+// Documentation โ https://docs.needle.tools/scripting
+
+exportclassCustomEventCallerextendsBehaviour{
+
+ // The next line is not just a comment, it defines
+ // a specific type for the component generator to use.
+
+ //@type MyCustomUnityEvent
+ @serializable(EventList)
+ myEvent!: EventList;
+
+ // just for testing - could be when a button is clicked, etc.
+ start(){
+ this.myEvent.invoke("Hello");
+ }
+}
+
+exportclassCustomEventReceiverextendsBehaviour{
+
+ logStringAndObject(str:string){
+ console.log("From Event: ", str);
+ }
+}
+
You can nest objects and their data. With properly matching @serializable(SomeType) decorators, the data will be serialized and deserialized into the correct types automatically.
Without the correct type decorators, you will still get the data, but just as a plain object. This is useful when you're porting components, as you'll have access to all data and can add types as required.
`,3),V={href:"https://stackblitz.com/edit/needle-engine-cycle-src?file=index.html",target:"_blank",rel:"noopener noreferrer"},G=n("h2",{id:"adding-new-postprocessing-effects",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#adding-new-postprocessing-effects","aria-hidden":"true"},"#"),s(" Adding new postprocessing effects")],-1),F={href:"https://github.com/pmndrs/postprocessing",target:"_blank",rel:"noopener noreferrer"},I=n("code",null,"npm i postprocessing",-1),U=n("code",null,"PostProcessingEffect",-1),N=n("p",null,[s("To use the effect add it to the same object as your "),n("code",null,"Volume"),s(" component.")],-1),W={href:"https://pmndrs.github.io/postprocessing/public/demo/#outline",target:"_blank",rel:"noopener noreferrer"},Y=e(`
import{ EffectProviderResult, PostProcessingEffect, registerCustomEffectType, serializable }from"@needle-tools/engine";
+import{ OutlineEffect }from"postprocessing";
+import{ Object3D }from"three";
+
+exportclassOutlinePostEffectextendsPostProcessingEffect{
+
+ // the outline effect takes a list of objects to outline
+ @serializable(Object3D)
+ selection!: Object3D[];
+
+ // this is just an example method that you could call to update the outline effect selection
+ updateSelection(){
+ if(this._outlineEffect){
+ this._outlineEffect.selection.clear();
+ for(const obj ofthis.selection){
+ this._outlineEffect.selection.add(obj);
+ }
+ }
+ }
+
+
+ // a unique name is required for custom effects
+ gettypeName():string{
+ return"Outline";
+ }
+
+ private _outlineEffect:void|undefined| OutlineEffect;
+
+ // method that creates the effect once
+ onCreateEffect(): EffectProviderResult |undefined{
+
+ const outlineEffect =newOutlineEffect(this.context.scene,this.context.mainCamera!);
+ this._outlineEffect = outlineEffect;
+ outlineEffect.edgeStrength =10;
+ outlineEffect.visibleEdgeColor.set(0xff0000);
+ for(const obj ofthis.selection){
+ outlineEffect.selection.add(obj);
+ }
+
+ return outlineEffect;
+ }
+}
+// You need to register your effect type with the engine
+registerCustomEffectType("Outline", OutlinePostEffect);
+
This is an example how you could create your own audio component. For most usecases however you can use the core AudioSource component and don't have to write code.
import{ AudioSource, Behaviour, serializable }from"@needle-tools/engine";
+
+// declaring AudioClip type is for codegen to produce the correct input field (for e.g. Unity or Blender)
+declaretypeAudioClip=string;
+
+exportclassMy2DAudioextendsBehaviour{
+
+ // The clip contains a string pointing to the audio file - by default it's relative to the GLB that contains the component
+ // by adding the URL decorator the clip string will be resolved relative to your project root and can be loaded
+ @serializable(URL)
+ clip?: AudioClip;
+
+ awake(){
+ // creating a new audio element and playing it
+ const audioElement =newAudio(this.clip);
+ audioElement.loop =true;
+ // on the web we have to wait for the user to interact with the page before we can play audio
+ AudioSource.registerWaitForAllowAudio(()=>{
+ audioElement.play();
+ })
+ }
+}
+
Use the FileReference type to load external files (e.g. a json file)
import{ Behaviour, FileReference, ImageReference, serializable }from"@needle-tools/engine";
+
+exportclassFileReferenceExampleextendsBehaviour{
+
+ // A FileReference can be used to load and assign arbitrary data in the editor. You can use it to load images, audio, text files... FileReference types will not be saved inside as part of the GLB (the GLB will only contain a relative URL to the file)
+ @serializable(FileReference)
+ myFile?: FileReference;
+ // Tip: if you want to export and load an image (that is not part of your GLB) if you intent to add it to your HTML content for example you can use the ImageReference type instead of FileReference. It will be loaded as an image and you can use it as a source for an <img> tag.
+
+ asyncstart(){
+ console.log("This is my file: ",this.myFile);
+ // load the file
+ const data =awaitthis.myFile?.loadRaw();
+ if(!data){
+ console.error("Failed loading my file...");
+ return;
+ }
+ console.log("Loaded my file. These are the bytes:",await data.arrayBuffer());
+ }
+}
+
import{ Behaviour, EventList, serializable, serializeable }from"@needle-tools/engine";
+
+exportclassHTMLButtonClickextendsBehaviour{
+
+ /** Enter a button query (e.g. button.some-button if you're interested in a button with the class 'some-button')
+ * Or you can also use an id (e.g. #some-button if you're interested in a button with the id 'some-button')
+ * Or you can also use a tag (e.g. button if you're interested in any button
+ */
+ @serializeable()
+ htmlSelector:string="button.some-button";
+
+ /** This is the event to be invoked when the html element is clicked. In Unity or Blender you can assign methods to be called in the Editor */
+ @serializable(EventList)
+ onClick: EventList =newEventList();
+
+ private element?: HTMLButtonElement;
+
+ onEnable(){
+ // Get the element from the DOM
+ this.element = document.querySelector(this.htmlSelector)as HTMLButtonElement;
+ if(this.element){
+ this.element.addEventListener('click',this.onClicked);
+ }
+ elseconsole.warn(\`Could not find element with selector \\"\${this.htmlSelector}\\"\`);
+ }
+
+ onDisable(){
+ if(this.element){
+ this.element.removeEventListener('click',this.onClicked);
+ }
+ }
+
+ privateonClicked=()=>{
+ this.onClick.invoke();
+ }
+}
+
`,23);function J(X,Z){const t=p("ExternalLinkIcon"),o=p("stackblitz"),c=p("video-embed");return l(),u("div",null,[d,v,n("ul",null,[n("li",null,[n("a",m,[s("Visit Samples Website"),a(t)])]),n("li",null,[n("a",b,[s("Download Samples Package"),a(t)])])]),h,a(o,{file:"@code/basic-component.ts"}),s(),y,n("div",w,[g,n("p",null,[s("Find a "),n("a",f,[s("working example in our samples"),a(t)]),s(" to download and try")])]),x,a(o,{file:"@code/component-click.ts"},{default:r(()=>[s(" test ")]),_:1}),q,n("div",_,[C,n("p",null,[s("EventList events are also invoked on the component level. This means you can also subscribe to the event declared above using "),S,s(" as well."),E,s(" This is an experimental feature: please provide feedback in our "),n("a",j,[s("discord"),a(t)])])]),B,n("div",O,[M,n("p",null,[s("Keep in mind that you still have access to all web apis and "),n("a",R,[s("npm"),a(t)]),s(" packages!"),L,s(" That's the beauty of Needle Engine if we're allowed to say this here ๐")])]),z,a(c,{src:"./videos/component-time.mp4",limit_height:""}),P,n("p",null,[s("Assuming you have a custom shader with a property name "),T,s(" that is a float value this is how you would change it from a script."),D,s(" You can find a live "),n("a",A,[s("example to download in our samples"),a(t)])]),H,n("p",null,[s("See "),n("a",V,[s("live example"),a(t)]),s(" on StackBlitz")]),G,n("p",null,[s("Make sure to install "),n("a",F,[I,a(t)]),s(" in your web project. Then you can add new effects by deriving from "),U,s(".")]),N,n("p",null,[s("Here is an example that wraps the "),n("a",W,[s("Outline postprocessing effect"),a(t)]),s(". You can expose variables and settings as usual as any effect is also just a component in your three.js scene.")]),Y,n("p",null,[s("Make sure to install the mediapipe package. Visit the github link below to see the complete project setup."),K,s(" Try it "),n("a",Q,[s("live here"),a(t)]),s(" - requires a webcam/camera")]),$])}const sn=i(k,[["render",J],["__file","scripting-examples.html.vue"]]);export{sn as default};
diff --git a/assets/scripting.html-4ae9b1ad.js b/assets/scripting.html-4ae9b1ad.js
new file mode 100644
index 000000000..511f04179
--- /dev/null
+++ b/assets/scripting.html-4ae9b1ad.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-fab6318a","path":"/scripting.html","title":"Needle Engine Scripting","lang":"en-US","frontmatter":{"title":"Needle Engine Scripting","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/needle engine scripting.png"}],["meta",{"name":"og:description","content":"---\\nIf you are new to scripting we recommend reading the following guides first:"}]],"description":"---\\nIf you are new to scripting we recommend reading the following guides first:"},"headers":[{"level":2,"title":"When you don't need to write code","slug":"when-you-don-t-need-to-write-code","link":"#when-you-don-t-need-to-write-code","children":[]},{"level":2,"title":"Creating a new component","slug":"creating-a-new-component","link":"#creating-a-new-component","children":[]},{"level":2,"title":"Component architecture","slug":"component-architecture","link":"#component-architecture","children":[{"level":3,"title":"Lifecycle methods","slug":"lifecycle-methods","link":"#lifecycle-methods","children":[]},{"level":3,"title":"Physic event methods","slug":"physic-event-methods","link":"#physic-event-methods","children":[]},{"level":3,"title":"Input event methods","slug":"input-event-methods","link":"#input-event-methods","children":[]},{"level":3,"title":"XR event methods","slug":"xr-event-methods","link":"#xr-event-methods","children":[]},{"level":3,"title":"Coroutines","slug":"coroutines","link":"#coroutines","children":[]}]},{"level":2,"title":"Special Lifecycle hooks","slug":"special-lifecycle-hooks","link":"#special-lifecycle-hooks","children":[]},{"level":2,"title":"Finding, adding and removing components","slug":"finding-adding-and-removing-components","link":"#finding-adding-and-removing-components","children":[{"level":3,"title":"Some of the available methods:","slug":"some-of-the-available-methods","link":"#some-of-the-available-methods","children":[]}]},{"level":2,"title":"Three.js and the HTML DOM","slug":"three.js-and-the-html-dom","link":"#three.js-and-the-html-dom","children":[{"level":3,"title":"Access the scene","slug":"access-the-scene","link":"#access-the-scene","children":[]},{"level":3,"title":"Time","slug":"time","link":"#time","children":[]},{"level":3,"title":"Input","slug":"input","link":"#input","children":[]},{"level":3,"title":"Physics","slug":"physics","link":"#physics","children":[]},{"level":3,"title":"Networking","slug":"networking","link":"#networking","children":[]}]},{"level":2,"title":"Accessing Needle Engine and components from anywhere","slug":"accessing-needle-engine-and-components-from-anywhere","link":"#accessing-needle-engine-and-components-from-anywhere","children":[]},{"level":2,"title":"Gizmos","slug":"gizmos","link":"#gizmos","children":[]},{"level":2,"title":"Serialization / Components in glTF files","slug":"serialization-components-in-gltf-files","link":"#serialization-components-in-gltf-files","children":[]},{"level":2,"title":"Loading Scenes","slug":"loading-scenes","link":"#loading-scenes","children":[]}],"git":{},"filePathRelative":"scripting.md"}`);export{e as data};
diff --git a/assets/scripting.html-64d5118f.js b/assets/scripting.html-64d5118f.js
new file mode 100644
index 000000000..76ebd58d8
--- /dev/null
+++ b/assets/scripting.html-64d5118f.js
@@ -0,0 +1,144 @@
+import{_ as p,M as i,p as l,q as d,R as e,N as s,V as c,t as n,a1 as t}from"./framework-c782e227.js";const r={},u=e("p",null,"If you are new to scripting we recommend reading the following guides first:",-1),h=e("hr",null,null,-1),m={href:"https://typescriptlang.org",target:"_blank",rel:"noopener noreferrer"},k={href:"https://javascript.info/",target:"_blank",rel:"noopener noreferrer"},b=e("p",null,"If you want to code-along with the following examples without having to install anything you just click the following link:",-1),v={href:"https://stackblitz.com/fork/github/needle-engine/vite-template?file=src%2Fmain.ts",target:"_blank",rel:"noopener noreferrer"},g=t('
Our web runtime engine adopts a component model similar to Unity and thus provides a lot of functionality that will feel familiar. Components attached to three's Object3D objects have lifecycle methods like awake, start, onEnable, onDisable, update and lateUpdate that you can implement. You can also use Coroutines.
Often, interactive scenes can be realized using Events in Unity and calling methods on built-in components. A typical example is playing an animation on button click - you create a button, add a Click event in the inspector, and have that call Animator.SetTrigger or similar to play a specific animation.
Needle Engine translates Unity Events into JavaScript method calls, which makes this a very fast and flexible workflow - set up your events as usual and when they're called they'll work the same as in Unity.
An example of a Button Click Event that is working out-of-the-box in Needle Engine โ no code needed.
Scripts are written in TypeScript (recommended) or JavaScript. There are two ways to add custom scripts to your project:
',9),y=e("li",null,[e("p",null,[n("Simply add a file with an "),e("code",null,".ts"),n(" or "),e("code",null,".js"),n(" extension inside "),e("code",null,"src/scripts/"),n(" in your generated project directory, for example "),e("code",null,"src/scripts/MyFirstScript.ts")])],-1),f=e("br",null,null,-1),w=e("br",null,null,-1),x=e("code",null,"Create > NPM Definition",-1),C=e("code",null,"Create > TypeScript",-1),_=e("p",null,[n("In both approaches, source directories are watched for changes and C# stub components or Blender panels are regenerated whenever a change is detected."),e("br"),n(" Changes to the source files also result in a hot reload of the running website โ you don't have to wait for Unity to recompile the C# components. This makes iterating on code pretty much instant.")],-1),j=e("p",null,[n("You can even have multiple component types inside one file (e.g. you can declare "),e("code",null,"export class MyComponent1"),n(" and "),e("code",null,"export class MyOtherComponent"),n(" in the same Typescript file).")],-1),R=t(`Example: Creating a Component that rotates an object
Create a component that rotates an object Create src/scripts/Rotate.ts and add the following code:
import{ Behaviour, serializable }from"@needle-tools/engine";
+
+exportclassRotateextendsBehaviour
+{
+ @serializable()
+ speed :number=1;
+
+ start(){
+ // logging this is useful for debugging in the browser.
+ // You can open the developer console (F12) to see what data your component contains
+ console.log(this);
+ }
+
+ // update will be called every frame
+ update(){
+ this.gameObject.rotateY(this.context.time.deltaTime *this.speed);
+ }
+}
+
Now inside Unity a new script called Rotate.cs will be automatically generated. Add the new Unity component to a Cube that is exported as part of a glTF file (it needs a GltfObject component in its parent) and save the scene. The cube is now rotating inside the browser. Open the chrome developer console by F12 to inspect the log from the Rotate.start method. This is a helpful practice to learn and debug what fields are exported and currently assigned. In general all public and serializable fields and all public properties are exported.
Now add a new field public float speed = 5 to your Unity component and save it. The Rotate component inspector now shows a speed field that you can edit. Save the scene (or click the Build button) and note that the javascript component now has the exported speed value assigned.
`,1),E={class:"custom-container details"},O=e("summary",null,"Create component with a custom function",-1),D=t(`
While generated C# components use the type name to produce stable GUIDs, we recommend checking in generated components in version control as a good practice.
Components are added to three.js Object3Ds. This is similar to how Components in Unity are added to GameObjects. Therefore when we want to access a three.js Object3D, we can access it as this.gameObject which returns the Object3D that the component is attached to.
Note: Setting visible to false on a Object3D will act like SetActive(false) in Unity - meaning it will also disable all the current components on this object and its children. Update events for inactive components are not being called until visible is set to true again. If you want to hide an object without affecting components you can just disable the Needle Engine Renderer component.
Note that lifecycle methods are only being called when they are declared. So only declare update lifecycle methods when they are actually necessary, otherwise it may hurt performance if you have many components with update loops that do nothing.
Method name
Description
awake()
First method being called when a new component is created
onEnable()
Called when a component is enabled (e.g. when enabled changes from false to true)
onDisable()
Called when a component is disabled (e.g. when enabled changes from true to false)
onDestroy()
called when the Object3D or component is being destroyed
start()
Called on the start of the first frame after the component was created
Optionally implement if you only want to receive XR callbacks for specific XR modes like immersive-vr or immersive-ar. Return true to notify the system that you want callbacks for the passed in mode
Callback when a controller is connected/added while in a XR session OR when the component joins a running XR session that has already connected controllers OR when the component becomes active during a running XR session that has already connected controllers
import{ Behaviour, FrameEvent }from"@needle-tools/engine";
+
+exportclassRotateextendsBehaviour{
+
+ start(){
+ // the second argument is optional and allows you to specifiy
+ // when it should be called in the current frame loop
+ // coroutine events are called after regular component events of the same name
+ // for example: Update coroutine events are called after component.update() functions
+ this.startCoroutine(this.rotate(), FrameEvent.Update);
+ }
+
+ // this method is called every frame until the component is disabled
+ *rotate(){
+ // keep looping forever
+ while(true){
+ yield;
+ }
+ }
+}
+
To stop a coroutine, either exit the routine by returning from it, or cache the return value of startCoroutine and call this.stopCoroutine(<...>). All Coroutines are stopped at onDisable / when disabling a component.
Needle Engine also exposes a few lifecycle hooks that you can use to hook into the update loop without having to write a full component. Those hooks can be inserted at any point in your web application (for example in toplevel scope or in a svelte component)
Method name
Description
onInitialized()
Called when a new context is initialized (before the first frame)
onStart()
Called directly after components start at the beginning of a frame (once per context)
onUpdate()
Called directly after components update
onBeforeRender()
called before calling render
For example:
// this can be put into e.g. main.ts or a svelte component (similar to onMount)
+import{ onUpdate }from"@needle-tools/engine"
+onUpdate((context:Context)=>{
+ // do something... e.g. access the scene via context.scene
+}
+
To access other components, use the static methods on GameObject or this.gameObject methods. For example, to access a Renderer component in the parent use GameObject.getComponentInParent(this.gameObject, Renderer) or this.gameObject.getComponentInParent(Renderer).
creates a new instance of this object including new instances of all its components
GameObject.destroy(Object3D | Component)
destroy a component or Object3D (and its components)
GameObject.addNewComponent(Object3D, Type)
adds (and creates) a new component for a type to the provided object. Note that awake and onEnable is already called when the component is returned
GameObject.addComponent(Object3D, Component)
moves a component instance to the provided object. It is useful if you already have an instance e.g. when you create a component with e.g. new MyComponent() and then attach it to a object
GameObject.removeComponent(Component)
removes a component from a gameObject
GameObject.getComponent(Object3D, Type)
returns the first component matching a type on the provided object.
GameObject.getComponents(Object3D, Type)
returns all components matching a type on the provided object.
GameObject.getComponentInChildren
same as getComponent but also searches in child objects.
GameObject.getComponentsInChildren
same as getComponents but also searches in child objects.
GameObject.getComponentInParent
same as getComponent but also searches in parent objects.
GameObject.getComponentsInParent
same as getComponents but also searches in parent objects.
This architecture allows for potentially having multiple needle WebGL scenes on the same webpage, that can either run on their own or communicate between each other as parts of your webpage.
To access the current scene from a component you use this.scene which is equivalent to this.context.scene, this gives you the root three.js scene object.
To traverse the hierarchy from a component you can either iterate over the children of an object with a for loop:
for(let i =0; i <this.gameObject.children; i++)
+ const ch =this.gameObject.children[i];
+
Another option that is quite useful when you just want to iterate objects being renderable you can query all renderer components and iterate over them like so:
Note that in this case you have to handle all cases yourself. For example you may need to use different events if your user is visiting your website on desktop vs mobile vs a VR device. These cases are automatically handled by the Needle Engine input events (e.g. PointerDown is raised both for mouse down, touch down and in case of VR on controller button down).
Use this.context.physics.raycast() to perform a raycast and get a list of intersections. If you dont pass in any options the raycast is performed from the mouse position (or first touch position) in screenspace using the currently active mainCamera. You can also pass in a RaycastOptions object that has various settings like maxDistance, the camera to be used or the layers to be tested against.
Note that the calls above are by default raycasting against visible scene objects. That is different to Unity where you always need colliders to hit objects. The default three.js solution has both pros and cons where one major con is that it can perform quite slow depending on your scene geometry. It may be especially slow when raycasting against skinned meshes. It is therefor recommended to usually set objects with SkinnedMeshRenderers in Unity to the Ignore Raycast layer which will then be ignored by default by Needle Engine as well.
Another option is to use the physics raycast methods which will only return hits with colliders in the scene.
const hit =this.context.physics.engine?.raycast();
+
# Accessing Needle Engine and components from anywhere
It is possible to access all the functionality described above using regular JavaScript code that is not inside components and lives somewhere else. All the components and functionality of the needle runtime is accessible via the global Needle namespace (you can write console.log(Needle) to get an overview)
You can find components using Needle.findObjectOfType(Needle.AudioSource) for example. It is recommended to cache those references, as searching the whole scene repeatedly is expensive. See the list for finding adding and removing components above.
For getting callbacks for the initial scene load see the following example:
You can also subscribe to the globale NeedleEngine (sometimes also referred to as ContextRegistry) to receive a callback when a Needle Engine context has been created or to access all available contexts:
Another option is using the onInitialized(ctx => {})lifecycle hook
You can also access all available contexts via NeedleEngine.Registered which returns the internal array. (Note that this array should not be modified but can be used to iterate all active contexts to modify settings, e.g. set all contexts to context.isPaused = true)
Below you find a list of available events on the static NeedleEngine type. You can subscribe to those events via NeedleEngine.registerCallback(ContextEvent.ContextCreated, (args) => {})
ContextEvent options
ContextEvent.ContextRegistered
Called when the context is registered to the registry.
ContextEvent.ContextCreationStart
Called before the first glb is loaded and can be used to initialize the physics engine. Can return a promise
ContextEvent.ContextCreated
Called when the context has been created before the first frame
ContextEvent.ContextDestroyed
Called when the context has been destroyed
ContextEvent.MissingCamera
Called when the context could not find a camera, currently only called during creation
ContextEvent.ContextClearing
Called when the context is being cleared: all objects in the scene are being destroyed and internal state is reset
The static Gizmos class can be used to draw lines, shapes and text which is mostly useful for debugging. All gizmos function have multiple options for e.g. colors or for how long they should be displayed in the scene. Internally they are cached and re-used.
Gizmos
Gizmos.DrawLabel
Draws a label with a background optionally. It can be attached to an object. Returns a Label handle which can be used to update the text.
Gizmos.DrawRay
Takes an origin and direction in worldspace to draw an infinite ray line
Gizmos.DrawDirection
Takes a origin and direction to draw a direction in worldspace
To embed components and recreate components with their correct types in glTF, we also need to save non-primitive types (everything that is not a Number, Boolean or String). You can do so is adding a @serializable(<type>) decorator above your field or property.
Example:
import{ Behaviour, serializable }from"@needle-tools/engine";
+import{ Object3D }from"three"
+
+exportclassMyClassextendsBehaviour{
+ // this will be a "Transform" field in Unity
+ @serializable(Object3D)
+ myObjectReference: Object3D |null=null;
+
+ // this will be a "Transform" array field in Unity
+ // Note that the @serializable decorator contains the array content type! (Object3D and not Object3D[])
+ @serializable(Object3D)
+ myObjectReferenceList: Object3D[]|null=null;
+}
+
To serialize from and to custom formats, it is possible to extend from the TypeSerializer class and create an instance. Use super() in the constructor to register supported types.
Note: In addition to matching fields, matching properties will also be exported when they match to fields in the typescript file.
These exported gltf files will be serialized as plain string URIs. To simplify loading these from TypeScript components, we added the concept of AssetReference types. They can be loaded at runtime and thus allow to defer loading parts of your app or loading external content.
Example:
import{ Behaviour, serializable, AssetReference }from"@needle-tools/engine";
+
+exportclassMyClassextendsBehaviour{
+
+ // if you export a prefab or scene as a reference from Unity you'll get a path to that asset
+ // which you can de-serialize to AssetReference for convenient loading
+ @serializable(AssetReference)
+ myPrefab?: AssetReference;
+
+ asyncstart(){
+ // directly instantiate
+ const myInstance =awaitthis.myPrefab?.instantiate();
+
+ // you can also just load and instantiate later
+ // const myInstance = await this.myPrefab.loadAssetAsync();
+ // this.gameObject.add(myInstance)
+ // this is useful if you know that you want to load this asset only once because it will not create a copy
+ // since \`\`instantiate()\`\` does create a copy of the asset after loading it
+ }
+}
+
AssetReferences are cached by URI, so if you reference the same exported glTF/Prefab in multiple components/scripts it will only be loaded once and then re-used.
`,4);function cn(pn,ln){const o=i("RouterLink"),a=i("ExternalLinkIcon");return l(),d("div",null,[u,e("ul",null,[e("li",null,[s(o,{to:"/getting-started/typescript-essentials.html"},{default:c(()=>[n("Typescript Essentials")]),_:1})]),e("li",null,[s(o,{to:"/getting-started/for-unity-developers.html"},{default:c(()=>[n("Needle Engine for Unity Developers")]),_:1})])]),h,e("p",null,[n("Runtime code for Needle Engine is written in "),e("a",m,[n("TypeScript"),s(a)]),n(" (recommended) or "),e("a",k,[n("JavaScript"),s(a)]),n(". We automatically generate C# stub components out of that, which you can add to GameObjects in the editor. The C# components and their data are recreated by the runtime as JavaScript components with the same data and attached to three.js objects.")]),e("p",null,[n("Both custom components as well as built-in Unity components can be mapped to JavaScript components in this way. For example, mappings for many built-in components related to animation, rendering or physics are already "),s(o,{to:"/component-reference.html#unity-components"},{default:c(()=>[n("included in Needle Engine")]),_:1}),n(".")]),b,e("ul",null,[e("li",null,[e("a",v,[n("Create virtual workspace to code along"),s(a)]),n(".")])]),g,e("ul",null,[y,e("li",null,[e("p",null,[n("Unity specific:"),f,n(" Organize your code into NPM Definition Files (npm packages). These help you to modularize and re-use code between projects and if you are familiar with web development they are in fact regular npm packages that are installed locally."),w,n(" In Unity you can create NpmDef files via "),x,n(" and then add TypeScript files by right-clicking an NpmDef file and selecting "),C,n(". Please see "),s(o,{to:"/project-structure.html#npm-definition-files"},{default:c(()=>[n("this chapter")]),_:1}),n(" for more information.")])])]),_,j,e("p",null,[n("If you are new to writing Javascript or Typescript we recommend reading the "),s(o,{to:"/getting-started/typescript-essentials.html"},{default:c(()=>[n("Typescript Essentials Guide")]),_:1}),n(" guide first before continuing with this guide.")]),R,e("details",E,[O,e("p",null,[n("Refer to the "),s(o,{to:"/getting-started/typescript-essentials.html"},{default:c(()=>[n("Typescript Essentials Guide")]),_:1}),n(" to learn more about the syntax and language.")]),D]),T,e("p",null,[n("Coroutines can be declared using the "),e("a",S,[n("JavaScript Generator Syntax"),s(a)]),n("."),N,n(" To start a coroutine, call "),q]),I,e("p",null,[n("The context refers to the runtime inside a "),e("a",z,[n("web component"),s(a)]),n("."),U,n(" The three.js scene lives inside a custom HTML component called "),G,n(" (see the "),P,n(" in your project). You can access the "),A,n(" web component using "),X,n(".")]),B,e("p",null,[n("You can also use three.js specific methods to quickly iterate all objects recursively using the "),e("a",M,[F,s(a)]),n(" method:")]),L,e("p",null,[n("or to just traverse visible objects use "),e("a",Y,[W,s(a)]),n(" instead.")]),V,e("p",null,[n("If you want to handle inputs yourself you can also subscribe to "),e("a",J,[n("all events the browser provides"),s(a)]),n(" (there are a ton). For example to subscribe to the browsers click event you can write:")]),H,e("p",null,[n("Use "),K,n(" to perform a raycast using a "),e("a",Q,[n("three.js ray"),s(a)])]),Z,e("p",null,[n("Here is a editable "),e("a",$,[n("example for physics raycast"),s(a)])]),nn,e("p",null,[n("Networking methods can be accessed via "),en,n(". Please refer to the "),s(o,{to:"/networking.html"},{default:c(()=>[n("networking docs")]),_:1}),n(" for further information.")]),sn,e("p",null,[n("Referenced Prefabs, SceneAssets and "),e("a",tn,[an,s(a)]),n(" in Unity will automatically be exported as glTF files (please refer to the "),s(o,{to:"/export.html"},{default:c(()=>[n("Export Prefabs")]),_:1}),n(" documentation).")]),on])}const rn=p(r,[["render",cn],["__file","scripting.html.vue"]]);export{rn as default};
diff --git a/assets/showcase-bike.html-85c8d128.js b/assets/showcase-bike.html-85c8d128.js
new file mode 100644
index 000000000..6903058fb
--- /dev/null
+++ b/assets/showcase-bike.html-85c8d128.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-48ca6631","path":"/showcase-bike.html","title":"Bike Configurator ๐ฒ","lang":"en-US","frontmatter":{"lang":"en-US","title":"Bike Configurator ๐ฒ","sidebar":false,"editLink":false,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/bike configurator.png"}],["meta",{"name":"og:description","content":"---\\nVisit website"}]],"description":"---\\nVisit website"},"headers":[{"level":3,"title":"Live","slug":"live","link":"#live","children":[]}],"git":{},"filePathRelative":"showcase-bike.md"}');export{e as data};
diff --git a/assets/showcase-bike.html-b9190120.js b/assets/showcase-bike.html-b9190120.js
new file mode 100644
index 000000000..ae84f1fa7
--- /dev/null
+++ b/assets/showcase-bike.html-b9190120.js
@@ -0,0 +1 @@
+import{_ as r,M as t,p as c,q as l,N as o,R as e,t as n}from"./framework-c782e227.js";const i={},_=e("h3",{id:"live",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#live","aria-hidden":"true"},"#"),n(" Live")],-1),d={href:"https://bike.needle.tools",target:"_blank",rel:"noopener noreferrer"};function h(p,m){const s=t("sample"),a=t("ExternalLinkIcon");return c(),l("div",null,[_,o(s,{src:"https://bike.needle.tools"}),e("p",null,[e("a",d,[n("Visit website"),o(a)])])])}const k=r(i,[["render",h],["__file","showcase-bike.html.vue"]]);export{k as default};
diff --git a/assets/showcase-castle.html-07c60c1d.js b/assets/showcase-castle.html-07c60c1d.js
new file mode 100644
index 000000000..1cc62f933
--- /dev/null
+++ b/assets/showcase-castle.html-07c60c1d.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-9d01ad8c","path":"/showcase-castle.html","title":"Castle Builder ๐ฐ","lang":"en-US","frontmatter":{"lang":"en-US","title":"Castle Builder ๐ฐ","sidebar":false,"editLink":false,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/castle builder.png"}],["meta",{"name":"og:description","content":"---\\nVisit website\\nBuild your own castle! Drag 3D models from the various palettes onto the stage, and create your very own world.\\nWorks on Desktop, Mobile, VR, AR, all right in your browser. Interactions are currently optimized for VR; placement on screens is a bit harder but possible. Have fun, no matter which device you're on!\\nInvite your friends! Click Create Room to be put into a live, multi-user space โ just copy the URL, send it to a friend and they join you automatically.\\nThere's currently a max limit for 'users online at the same time'"}]],"description":"---\\nVisit website\\nBuild your own castle! Drag 3D models from the various palettes onto the stage, and create your very own world.\\nWorks on Desktop, Mobile, VR, AR, all right in your browser. Interactions are currently optimized for VR; placement on screens is a bit harder but possible. Have fun, no matter which device you're on!\\nInvite your friends! Click Create Room to be put into a live, multi-user space โ just copy the URL, send it to a friend and they join you automatically.\\nThere's currently a max limit for 'users online at the same time'"},"headers":[{"level":3,"title":"Live","slug":"live","link":"#live","children":[]},{"level":3,"title":"How To Play","slug":"how-to-play","link":"#how-to-play","children":[]},{"level":3,"title":"Info","slug":"info","link":"#info","children":[]}],"git":{},"filePathRelative":"showcase-castle.md"}`);export{e as data};
diff --git a/assets/showcase-castle.html-bbb49996.js b/assets/showcase-castle.html-bbb49996.js
new file mode 100644
index 000000000..53c4786a6
--- /dev/null
+++ b/assets/showcase-castle.html-bbb49996.js
@@ -0,0 +1 @@
+import{_ as r,M as t,p as i,q as l,N as n,R as e,t as o}from"./framework-c782e227.js";const c={},d=e("h3",{id:"live",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#live","aria-hidden":"true"},"#"),o(" Live")],-1),h={href:"https://castle.needle.tools",target:"_blank",rel:"noopener noreferrer"},u=e("h3",{id:"how-to-play",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#how-to-play","aria-hidden":"true"},"#"),o(" How To Play")],-1),_=e("p",null,"Build your own castle! Drag 3D models from the various palettes onto the stage, and create your very own world. Works on Desktop, Mobile, VR, AR, all right in your browser. Interactions are currently optimized for VR; placement on screens is a bit harder but possible. Have fun, no matter which device you're on!",-1),p=e("p",null,`Invite your friends! Click Create Room to be put into a live, multi-user space โ just copy the URL, send it to a friend and they join you automatically. There's currently a max limit for "users online at the same time" - if you don't get into a room, please try later.`,-1),m=e("h3",{id:"info",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#info","aria-hidden":"true"},"#"),o(" Info")],-1),f=e("p",null,"This page was authored in Unity and exported to three.js using tools and technologies by ๐ต needle.",-1),y=e("p",null,"There are a lot of open technologies involved: 3D models are in glTF format, the render engine is three.js, VR and AR are using WebXR. The networking server runs on Glitch, and audio is sent over WebRTC using PeerJS.",-1);function v(b,g){const a=t("sample"),s=t("ExternalLinkIcon");return i(),l("div",null,[d,n(a,{src:"https://castle.needle.tools"}),e("p",null,[e("a",h,[o("Visit website"),n(s)])]),u,_,p,m,f,y])}const x=r(c,[["render",v],["__file","showcase-castle.html.vue"]]);export{x as default};
diff --git a/assets/showcase-mercedes-benz.html-5df967f5.js b/assets/showcase-mercedes-benz.html-5df967f5.js
new file mode 100644
index 000000000..7f58ad004
--- /dev/null
+++ b/assets/showcase-mercedes-benz.html-5df967f5.js
@@ -0,0 +1,30 @@
+import{_ as i,M as n,p,q as c,R as s,t as e,N as a,a1 as r}from"./framework-c782e227.js";const l="/docs/showcase-mercedes/1_skybox.png",d="/docs/showcase-mercedes/2_paintjob_simple.jpg",h="/docs/showcase-mercedes/3_SpecularHighlights_off.jpg",u="/docs/showcase-mercedes/4_SpecularHighlights_on.jpg",m="/docs/showcase-mercedes/5_NoBackground.jpg",k="/docs/showcase-mercedes/6_MapBackground.png",g="/docs/showcase-mercedes/7_EnvShaderGraph.jpg",w="/docs/showcase-mercedes/8_Gradiant.png",b="/docs/showcase-mercedes/9_Rotator.png",v="/docs/showcase-mercedes/10_WheelsAndGrid.png",f="/docs/showcase-mercedes/11_GridShader.jpg",y="/docs/showcase-mercedes/12_WheelWithText.png",_="/docs/showcase-mercedes/13_WheelShader.jpg",x="/docs/showcase-mercedes/14_RearUI.jpg",j={},W=s("h2",{id:"about",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#about","aria-hidden":"true"},"#"),e(" About")],-1),T={href:"https://www.ishowroom.cz/home/",target:"_blank",rel:"noopener noreferrer"},R=r('
I'm not very well experienced with javascript, typescript or three.js, so my point of view is as a semi-experienced Unity developer trying out the simplest way how to create a web experience. For those who would suggest Unity WebGL, that sadly doesn't work and isn't flexible on mobile browsers. Needle is ๐
Our lighting model is based on reflection probes in unity. We do not need any directional or point lights, only ambient lighting.
We're using this skybox:
Which looks like this on the paint job:
Then to add a slight detail, i've added 2 directional lights with an insignificant intensity (0.04) to create specular highlights. So before it looked like this:
But with the added directional lights it added a better dynamic. The effect could be deepened with higher intensity:
The black background isn't very pretty. So to differentiate between visual and lighting skyboxes i've added an inverse sphere which wraps the whole map.
Regarding the gradient goes from a slight gray to a white color..
This effect could be easily made with just a proper UV mapping and a single pixel high texture which would define the gradient.
I've made an unlit shader in the shader graph:
I've noticed a color banding issue, so i've tried to implement dithering. Frankly, it didn't help the artefacts but i bet there's a simple solution to that issue. So the upper part of the shader does sample the gradient based on the Y axis in object space. And the lower part tries to negate the color banding.
By using shaders it's simpler to use and iterate the gradiant. By using Needle's Shadergraph markdown asset, it's even simpler! ๐ต
The user now sees a car driving in deep nothingness, the color doesn't resemble anything and the experience is dull. We want to ground the model and that's done by adding a grid and then shifting it so it seems the car is moving. This is what we want to achieve:
The shader for the grid was comprised of two parts. A simple tiled texture of the grid that's being multipled by a circular gradient to make the edges fade off.
This tech demo takes it's goal to showcase the car's capabilities.
Let's start by highlighting the wheels.
Adding this shader to a plane will result in a dashed circle which is rotating by a defined speed. Combined with world space UI with a normal Text component this can highlight some interesting capabilities or parameters of the given product.
After showcasing the wheels we want to finish with a broad information about the product. In this case, that would be the car's full name and perhaps some available configurations.
Needle Engine seems to be a very good candidate for us!
There are a few features which we miss.
That would be for example proper support for the Lit Shader Graphs. But nothing stops us to create shaders the three.js way and create simmilar shaders in Unity for our content team to tweak the materials.
Using Needle was a blast! ๐ต
',48);function S(A,B){const t=n("ExternalLinkIcon"),o=n("sample");return p(),c("div",null,[W,s("p",null,[e("Hello, my name is Kryลกtof and i did a research project about Needle. At "),s("a",T,[e("our company"),a(t)]),e(", we wanted to determine how Needle can help us in our workflow. We have one local client which focuses on reselling luxury cars. We already delivered a mobile app and VR experience using Unity. We have around 30 unique cars ready in the engine. We plan to expand the client's website with visually pleasing digital clones with more configuration options. Needle could achieve a perfect 1:1 conversion between unity and web visuals. It would be a massive benefit to our workflow. So that's what sparked our research.")]),a(o,{src:"https://engine.needle.tools/demos/mercedes-benz-demo/"}),R])}const I=i(j,[["render",S],["__file","showcase-mercedes-benz.html.vue"]]);export{I as default};
diff --git a/assets/showcase-mercedes-benz.html-885e91f2.js b/assets/showcase-mercedes-benz.html-885e91f2.js
new file mode 100644
index 000000000..70a4fda5e
--- /dev/null
+++ b/assets/showcase-mercedes-benz.html-885e91f2.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-aa56fb8c","path":"/showcase-mercedes-benz.html","title":"Mercedes-Benz Showcase","lang":"en-US","frontmatter":{"lang":"en-US","title":"Mercedes-Benz Showcase","editLink":false,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/mercedes benz showcase.png"}],["meta",{"name":"og:description","content":"---\\nHello, my name is Kryลกtof and i did a research project about Needle. At our company, we wanted to determine how Needle can help us in our workflow. We have one local client which focuses on reselling luxury cars. We already delivered a mobile app and VR experience using Unity. We have around 30 unique cars ready in the engine. We plan to expand the client's website with visually pleasing digital clones with more configuration options. Needle could achieve a perfect 1:1 conversion between unity and web visuals. It would be a massive benefit to our workflow. So that's what sparked our research.\\nI'm not very well experienced with javascript, typescript or three.js, so my point of view is as a semi-experienced Unity developer trying out the simplest way how to create a web experience. For those who would suggest Unity WebGL, that sadly doesn't work and isn't flexible on mobile browsers. Needle is ๐\\nOur lighting model is based on reflection probes in unity. We do not need any directional or point lights, only ambient lighting.\\nWe're using this skybox:\\nWhich looks like this on the paint job:\\nThen to add a slight detail, i've added 2 directional lights with an insignificant intensity (0.04) to create specular highlights. So before it looked like this:\\nBut with the added directional lights it added a better dynamic. The effect could be deepened with higher intensity:\\nThe scene now looks like this:\\nThe black background isn't very pretty. So to differentiate b"}]],"description":"---\\nHello, my name is Kryลกtof and i did a research project about Needle. At our company, we wanted to determine how Needle can help us in our workflow. We have one local client which focuses on reselling luxury cars. We already delivered a mobile app and VR experience using Unity. We have around 30 unique cars ready in the engine. We plan to expand the client's website with visually pleasing digital clones with more configuration options. Needle could achieve a perfect 1:1 conversion between unity and web visuals. It would be a massive benefit to our workflow. So that's what sparked our research.\\nI'm not very well experienced with javascript, typescript or three.js, so my point of view is as a semi-experienced Unity developer trying out the simplest way how to create a web experience. For those who would suggest Unity WebGL, that sadly doesn't work and isn't flexible on mobile browsers. Needle is ๐\\nOur lighting model is based on reflection probes in unity. We do not need any directional or point lights, only ambient lighting.\\nWe're using this skybox:\\nWhich looks like this on the paint job:\\nThen to add a slight detail, i've added 2 directional lights with an insignificant intensity (0.04) to create specular highlights. So before it looked like this:\\nBut with the added directional lights it added a better dynamic. The effect could be deepened with higher intensity:\\nThe scene now looks like this:\\nThe black background isn't very pretty. So to differentiate b"},"headers":[{"level":2,"title":"About","slug":"about","link":"#about","children":[]},{"level":2,"title":"Context","slug":"context","link":"#context","children":[]},{"level":2,"title":"Lighting","slug":"lighting","link":"#lighting","children":[]},{"level":2,"title":"Background","slug":"background","link":"#background","children":[]},{"level":2,"title":"Car fake movement","slug":"car-fake-movement","link":"#car-fake-movement","children":[]},{"level":2,"title":"Extra elements","slug":"extra-elements","link":"#extra-elements","children":[]},{"level":2,"title":"Wrap up","slug":"wrap-up","link":"#wrap-up","children":[]},{"level":2,"title":"Conclusion","slug":"conclusion","link":"#conclusion","children":[]}],"git":{},"filePathRelative":"showcase-mercedes-benz.md"}`);export{e as data};
diff --git a/assets/showcase-monsterhands.html-20c4670d.js b/assets/showcase-monsterhands.html-20c4670d.js
new file mode 100644
index 000000000..698c321fb
--- /dev/null
+++ b/assets/showcase-monsterhands.html-20c4670d.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-da75c730","path":"/showcase-monsterhands.html","title":"Monster Hands ๐","lang":"en-US","frontmatter":{"lang":"en-US","title":"Monster Hands ๐","sidebar":false,"editLink":false,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/monster hands.png"}],["meta",{"name":"og:description","content":"---\\nVisit website"}]],"description":"---\\nVisit website"},"headers":[{"level":3,"title":"Live","slug":"live","link":"#live","children":[]}],"git":{},"filePathRelative":"showcase-monsterhands.md"}');export{e as data};
diff --git a/assets/showcase-monsterhands.html-d862399e.js b/assets/showcase-monsterhands.html-d862399e.js
new file mode 100644
index 000000000..de529a277
--- /dev/null
+++ b/assets/showcase-monsterhands.html-d862399e.js
@@ -0,0 +1 @@
+import{_ as r,M as n,p as c,q as l,N as t,R as e,t as s}from"./framework-c782e227.js";const _={},d=e("h3",{id:"live",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#live","aria-hidden":"true"},"#"),s(" Live")],-1),h={href:"https://monster-hands.needle.tools/",target:"_blank",rel:"noopener noreferrer"};function i(m,p){const o=n("sample"),a=n("ExternalLinkIcon");return c(),l("div",null,[d,t(o,{src:"https://monster-hands.needle.tools/"}),e("p",null,[e("a",h,[s("Visit website"),t(a)])])])}const u=r(_,[["render",i],["__file","showcase-monsterhands.html.vue"]]);export{u as default};
diff --git a/assets/showcase-towerdefence.html-5d7f7b90.js b/assets/showcase-towerdefence.html-5d7f7b90.js
new file mode 100644
index 000000000..94e23185c
--- /dev/null
+++ b/assets/showcase-towerdefence.html-5d7f7b90.js
@@ -0,0 +1 @@
+import{_ as a,M as t,p as c,q as l,N as n,R as e,t as o}from"./framework-c782e227.js";const i={},d=e("h3",{id:"live",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#live","aria-hidden":"true"},"#"),o(" Live")],-1),_={href:"https://willitaugment.itch.io/tumbleweed-defender",target:"_blank",rel:"noopener noreferrer"};function h(m,p){const s=t("sample"),r=t("ExternalLinkIcon");return c(),l("div",null,[d,n(s,{src:"https://v6p9d9t4.ssl.hwcdn.net/html/7746989/index.html"}),e("p",null,[e("a",_,[o("Visit website"),n(r)])])])}const u=a(i,[["render",h],["__file","showcase-towerdefence.html.vue"]]);export{u as default};
diff --git a/assets/showcase-towerdefence.html-cda32285.js b/assets/showcase-towerdefence.html-cda32285.js
new file mode 100644
index 000000000..341429c79
--- /dev/null
+++ b/assets/showcase-towerdefence.html-cda32285.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-3d5fe3ab","path":"/showcase-towerdefence.html","title":"Tower Defense","lang":"en-US","frontmatter":{"lang":"en-US","title":"Tower Defense","sidebar":false,"editLink":false,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/tower defense.png"}],["meta",{"name":"og:description","content":"---\\nVisit website"}]],"description":"---\\nVisit website"},"headers":[{"level":3,"title":"Live","slug":"live","link":"#live","children":[]}],"git":{},"filePathRelative":"showcase-towerdefence.md"}');export{e as data};
diff --git a/assets/showcase-website.html-5e0d8fd1.js b/assets/showcase-website.html-5e0d8fd1.js
new file mode 100644
index 000000000..de450f268
--- /dev/null
+++ b/assets/showcase-website.html-5e0d8fd1.js
@@ -0,0 +1 @@
+import{_ as r,M as t,p as c,q as l,N as o,R as e,t as s}from"./framework-c782e227.js";const i={},_=e("h3",{id:"live",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#live","aria-hidden":"true"},"#"),s(" Live")],-1),d={href:"https://needle.tools",target:"_blank",rel:"noopener noreferrer"};function h(p,m){const n=t("sample"),a=t("ExternalLinkIcon");return c(),l("div",null,[_,o(n,{src:"https://needle.tools"}),e("p",null,[e("a",d,[s("Visit website"),o(a)])])])}const u=r(i,[["render",h],["__file","showcase-website.html.vue"]]);export{u as default};
diff --git a/assets/showcase-website.html-a77e83b5.js b/assets/showcase-website.html-a77e83b5.js
new file mode 100644
index 000000000..e4c65697b
--- /dev/null
+++ b/assets/showcase-website.html-a77e83b5.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-499adc36","path":"/showcase-website.html","title":"Castle Builder ๐ฐ","lang":"en-US","frontmatter":{"lang":"en-US","title":"Castle Builder ๐ฐ","sidebar":false,"editLink":false,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/castle builder.png"}],["meta",{"name":"og:description","content":"---\\nVisit website"}]],"description":"---\\nVisit website"},"headers":[{"level":3,"title":"Live","slug":"live","link":"#live","children":[]}],"git":{},"filePathRelative":"showcase-website.md"}');export{e as data};
diff --git a/assets/showcase-zenrepublic.html-053f7194.js b/assets/showcase-zenrepublic.html-053f7194.js
new file mode 100644
index 000000000..b7f742224
--- /dev/null
+++ b/assets/showcase-zenrepublic.html-053f7194.js
@@ -0,0 +1 @@
+import{_ as c,M as n,p as o,q as l,N as t,R as e,t as r}from"./framework-c782e227.js";const i={},_=e("h3",{id:"live",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#live","aria-hidden":"true"},"#"),r(" Live")],-1),p={href:"https://zenrepublic.space/?realm=3",target:"_blank",rel:"noopener noreferrer"};function h(d,m){const a=n("sample"),s=n("ExternalLinkIcon");return o(),l("div",null,[_,t(a,{src:"https://zenrepublic.space/?realm=3"}),e("p",null,[e("a",p,[r("Visit website"),t(s)])])])}const f=c(i,[["render",h],["__file","showcase-zenrepublic.html.vue"]]);export{f as default};
diff --git a/assets/showcase-zenrepublic.html-9b4867f5.js b/assets/showcase-zenrepublic.html-9b4867f5.js
new file mode 100644
index 000000000..e908457db
--- /dev/null
+++ b/assets/showcase-zenrepublic.html-9b4867f5.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-7c306381","path":"/showcase-zenrepublic.html","title":"Monster Hands ๐","lang":"en-US","frontmatter":{"lang":"en-US","title":"Monster Hands ๐","sidebar":false,"editLink":false,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/monster hands.png"}],["meta",{"name":"og:description","content":"---\\nVisit website"}]],"description":"---\\nVisit website"},"headers":[{"level":3,"title":"Live","slug":"live","link":"#live","children":[]}],"git":{},"filePathRelative":"showcase-zenrepublic.md"}');export{e as data};
diff --git a/assets/stackblitz-a07b5d35.js b/assets/stackblitz-a07b5d35.js
new file mode 100644
index 000000000..051225cda
--- /dev/null
+++ b/assets/stackblitz-a07b5d35.js
@@ -0,0 +1,33 @@
+import{_ as h,p as f,q as b,R as g,s as w}from"./framework-c782e227.js";function x(e){return e.startsWith("@code")?e.replace(/^@code/,"./code-samples/"):e}async function y(e,n,t,o){let s=await(await fetch(n)).text();o&&(s=o(s)),t[e]=s}async function c(e,n,t,o,a){e.push(y(n,t,o,a))}async function k(e,n){const t=[];c(t,"package.json","https://raw.githubusercontent.com/needle-engine/vite-template/main/package.json",e),c(t,"package-lock.json","https://raw.githubusercontent.com/needle-engine/vite-template/main/package-lock.json",e),c(t,"src/styles/style.css","https://raw.githubusercontent.com/needle-engine/vite-template/main/src/styles/style.css",e),c(t,"vite.config.js","https://raw.githubusercontent.com/needle-engine/vite-template/main/vite.config.js",e,o=>o.replace("needlePlugins(command, needleConfig)","needlePlugins(command, needleConfig, { noPoster: true })")),c(t,"tsconfig.json","https://raw.githubusercontent.com/needle-engine/vite-template/main/tsconfig.json",e),c(t,"index.html","https://raw.githubusercontent.com/needle-engine/vite-template/main/index.html",e,o=>o.replace(/\/,``)),await Promise.all(t)}async function _(e,n){var p,u;let t=`// Generated via ${window.location.href} at ${new Date().toISOString()}
+import { NeedleEngine, RemoteSkybox, GameObject, ObjectRaycaster } from '@needle-tools/engine';
+import * as THREE from 'three';
+`;const o=x(n),a=window.location,s=a.pathname.split("/").slice(0,-1).join("/"),i=`${a.origin}${s}/${o}`,r=await(await fetch(i)).text(),d="src/main.ts",l=r.match(/export class\s+?(?.+?)\s+extends Behaviour/),m=(u=(p=l==null?void 0:l.groups)==null?void 0:p.component_name)==null?void 0:u.trim();return console.log(m),t+=`
+// SAMPLE SCRIPT START
+`+r+`
+// SAMPLE SCRIPT END
+`,t+=`
+
+NeedleEngine.addContextCreatedCallback((args) => {
+ const context = args.context;
+ const scene = context.scene;
+
+ const grid = new THREE.GridHelper();
+ scene.add(grid);
+
+ const geometry = new THREE.BoxGeometry(1, 1, 1);
+ const material = new THREE.MeshStandardMaterial({ color: 0xdddddd });
+ const cube = new THREE.Mesh(geometry, material);
+ cube.name = "Cube";
+ cube.position.y += 0.5;
+ scene.add(cube);
+ GameObject.addComponent(cube, new ${m}());
+ ${r.includes("IPointerClickHandler")?"GameObject.addNewComponent(cube, ObjectRaycaster)":""}
+
+ const remoteSkybox = new RemoteSkybox();
+ remoteSkybox.background = false;
+ remoteSkybox.url =
+ 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/cyclorama_hard_light_1k.hdr';
+ GameObject.addComponent(grid, remoteSkybox);
+});
+`,e[d]=t,d}const j={props:{file:String},methods:{async openProject(){const e={};await k(e,"https://github.com/needle-engine/vite-template/raw/main/assets/basic.glb");const n=await _(e,this.file),t=n.split("/").pop();StackBlitzSDK.openProject({files:{...e,"index.ts":`import './src/main';
+import '${n}';`},template:"node",title:`${t}`,description:"This is a generated project via https://docs.needle.engine. Please note that this feature is experimental(!) and the project might not work as expected."},{newWindow:!0,openFile:n})}}},P={class:"code"};function S(e,n,t,o,a,s){return f(),b("div",null,[g("button",{onClick:n[0]||(n[0]=(...i)=>s.openProject&&s.openProject(...i))}," Open in StackBlitz (Experimental) "),g("div",P,[w(e.$slots,"default")])])}const C=h(j,[["render",S],["__file","stackblitz.vue"]]);export{C as default};
diff --git a/assets/style-a89311da.css b/assets/style-a89311da.css
new file mode 100644
index 000000000..7729d4dc1
--- /dev/null
+++ b/assets/style-a89311da.css
@@ -0,0 +1 @@
+:root{--back-to-top-z-index: 5;--back-to-top-color: #3eaf7c;--back-to-top-color-hover: #71cda3}.back-to-top{cursor:pointer;position:fixed;bottom:2rem;right:2.5rem;width:2rem;height:1.2rem;background-color:var(--back-to-top-color);-webkit-mask:url(/docs/assets/back-to-top-8efcbe56.svg) no-repeat;mask:url(/docs/assets/back-to-top-8efcbe56.svg) no-repeat;z-index:var(--back-to-top-z-index)}.back-to-top:hover{background-color:var(--back-to-top-color-hover)}@media (max-width: 959px){.back-to-top{display:none}}@media print{.back-to-top{display:none}}.back-to-top-enter-active,.back-to-top-leave-active{transition:opacity .3s}.back-to-top-enter-from,.back-to-top-leave-to{opacity:0}:root{--external-link-icon-color: #aaa}.external-link-icon{position:relative;display:inline-block;color:var(--external-link-icon-color);vertical-align:middle;top:-1px}@media print{.external-link-icon{display:none}}.external-link-icon-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}:root{--medium-zoom-z-index: 100;--medium-zoom-bg-color: #ffffff;--medium-zoom-opacity: 1}.medium-zoom-overlay{background-color:var(--medium-zoom-bg-color)!important;z-index:var(--medium-zoom-z-index)}.medium-zoom-overlay~img{z-index:calc(var(--medium-zoom-z-index) + 1)}.medium-zoom--opened .medium-zoom-overlay{opacity:var(--medium-zoom-opacity)}:root{--nprogress-color: #29d;--nprogress-z-index: 1031}#nprogress{pointer-events:none}#nprogress .bar{background:var(--nprogress-color);position:fixed;z-index:var(--nprogress-z-index);top:0;left:0;width:100%;height:2px}:root{--c-brand: #3eaf7c;--c-brand-light: #4abf8a;--c-bg: #ffffff;--c-bg-light: #f3f4f5;--c-bg-lighter: #eeeeee;--c-bg-dark: #ebebec;--c-bg-darker: #e6e6e6;--c-bg-navbar: var(--c-bg);--c-bg-sidebar: var(--c-bg);--c-bg-arrow: #cccccc;--c-text: #2c3e50;--c-text-accent: var(--c-brand);--c-text-light: #3a5169;--c-text-lighter: #4e6e8e;--c-text-lightest: #6a8bad;--c-text-quote: #999999;--c-border: #eaecef;--c-border-dark: #dfe2e5;--c-tip: #42b983;--c-tip-bg: var(--c-bg-light);--c-tip-title: var(--c-text);--c-tip-text: var(--c-text);--c-tip-text-accent: var(--c-text-accent);--c-warning: #ffc310;--c-warning-bg: #fffae3;--c-warning-bg-light: #fff3ba;--c-warning-bg-lighter: #fff0b0;--c-warning-border-dark: #f7dc91;--c-warning-details-bg: #fff5ca;--c-warning-title: #f1b300;--c-warning-text: #746000;--c-warning-text-accent: #edb100;--c-warning-text-light: #c1971c;--c-warning-text-quote: #ccab49;--c-danger: #f11e37;--c-danger-bg: #ffe0e0;--c-danger-bg-light: #ffcfde;--c-danger-bg-lighter: #ffc9c9;--c-danger-border-dark: #f1abab;--c-danger-details-bg: #ffd4d4;--c-danger-title: #ed1e2c;--c-danger-text: #660000;--c-danger-text-accent: #bd1a1a;--c-danger-text-light: #b5474d;--c-danger-text-quote: #c15b5b;--c-details-bg: #eeeeee;--c-badge-tip: var(--c-tip);--c-badge-warning: #ecc808;--c-badge-warning-text: var(--c-bg);--c-badge-danger: #dc2626;--c-badge-danger-text: var(--c-bg);--t-color: .3s ease;--t-transform: .3s ease;--code-bg-color: #282c34;--code-hl-bg-color: rgba(0, 0, 0, .66);--code-ln-color: #9e9e9e;--code-ln-wrapper-width: 3.5rem;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;--font-family-code: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;--navbar-height: 3.6rem;--navbar-padding-v: .7rem;--navbar-padding-h: 1.5rem;--sidebar-width: 20rem;--sidebar-width-mobile: calc(var(--sidebar-width) * .82);--content-width: 740px;--homepage-width: 960px}.back-to-top{--back-to-top-color: var(--c-brand);--back-to-top-color-hover: var(--c-brand-light)}.DocSearch{--docsearch-primary-color: var(--c-brand);--docsearch-text-color: var(--c-text);--docsearch-highlight-color: var(--c-brand);--docsearch-muted-color: var(--c-text-quote);--docsearch-container-background: rgba(9, 10, 17, .8);--docsearch-modal-background: var(--c-bg-light);--docsearch-searchbox-background: var(--c-bg-lighter);--docsearch-searchbox-focus-background: var(--c-bg);--docsearch-searchbox-shadow: inset 0 0 0 2px var(--c-brand);--docsearch-hit-color: var(--c-text-light);--docsearch-hit-active-color: var(--c-bg);--docsearch-hit-background: var(--c-bg);--docsearch-hit-shadow: 0 1px 3px 0 var(--c-border-dark);--docsearch-footer-background: var(--c-bg)}.external-link-icon{--external-link-icon-color: var(--c-text-quote)}.medium-zoom-overlay{--medium-zoom-bg-color: var(--c-bg)}#nprogress{--nprogress-color: var(--c-brand)}.pwa-popup{--pwa-popup-text-color: var(--c-text);--pwa-popup-bg-color: var(--c-bg);--pwa-popup-border-color: var(--c-brand);--pwa-popup-shadow: 0 4px 16px var(--c-brand);--pwa-popup-btn-text-color: var(--c-bg);--pwa-popup-btn-bg-color: var(--c-brand);--pwa-popup-btn-hover-bg-color: var(--c-brand-light)}.search-box{--search-bg-color: var(--c-bg);--search-accent-color: var(--c-brand);--search-text-color: var(--c-text);--search-border-color: var(--c-border);--search-item-text-color: var(--c-text-lighter);--search-item-focus-bg-color: var(--c-bg-light)}html.dark{--c-brand: #3aa675;--c-brand-light: #349469;--c-bg: #22272e;--c-bg-light: #2b313a;--c-bg-lighter: #262c34;--c-bg-dark: #343b44;--c-bg-darker: #37404c;--c-text: #adbac7;--c-text-light: #96a7b7;--c-text-lighter: #8b9eb0;--c-text-lightest: #8094a8;--c-border: #3e4c5a;--c-border-dark: #34404c;--c-tip: #318a62;--c-warning: #e0ad15;--c-warning-bg: #2d2f2d;--c-warning-bg-light: #423e2a;--c-warning-bg-lighter: #44442f;--c-warning-border-dark: #957c35;--c-warning-details-bg: #39392d;--c-warning-title: #fdca31;--c-warning-text: #d8d96d;--c-warning-text-accent: #ffbf00;--c-warning-text-light: #ddb84b;--c-warning-text-quote: #ccab49;--c-danger: #fc1e38;--c-danger-bg: #39232c;--c-danger-bg-light: #4b2b35;--c-danger-bg-lighter: #553040;--c-danger-border-dark: #a25151;--c-danger-details-bg: #482936;--c-danger-title: #fc2d3b;--c-danger-text: #ea9ca0;--c-danger-text-accent: #fd3636;--c-danger-text-light: #d9777c;--c-danger-text-quote: #d56b6b;--c-details-bg: #323843;--c-badge-warning: var(--c-warning);--c-badge-warning-text: #3c2e05;--c-badge-danger: var(--c-danger);--c-badge-danger-text: #401416;--code-hl-bg-color: #363b46}html.dark .DocSearch{--docsearch-logo-color: var(--c-text);--docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;--docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d, 0 2px 2px 0 rgba(3, 4, 9, .3);--docsearch-key-gradient: linear-gradient(-225deg, #444950, #1c1e21);--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2)}html,body{padding:0;margin:0;background-color:var(--c-bg);transition:background-color var(--t-color)}html.dark{color-scheme:dark}html{font-size:16px}body{font-family:var(--font-family);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:1rem;color:var(--c-text)}a{font-weight:500;color:var(--c-text-accent);text-decoration:none;overflow-wrap:break-word}p a code{font-weight:400;color:var(--c-text-accent)}kbd{font-family:var(--font-family-code);color:var(--c-text);background:var(--c-bg-lighter);border:solid .15rem var(--c-border-dark);border-bottom:solid .25rem var(--c-border-dark);border-radius:.15rem;padding:0 .15em}code{font-family:var(--font-family-code);color:var(--c-text-lighter);padding:.25rem .5rem;margin:0;font-size:.85em;background-color:var(--c-bg-light);border-radius:3px;overflow-wrap:break-word;transition:background-color var(--t-color)}blockquote{font-size:1rem;color:var(--c-text-quote);border-left:.2rem solid var(--c-border-dark);margin:1rem 0;padding:.25rem 0 .25rem 1rem;overflow-wrap:break-word}blockquote>p{margin:0}ul,ol{padding-left:1.2em}strong{font-weight:600}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.25;overflow-wrap:break-word}h1:focus-visible,h2:focus-visible,h3:focus-visible,h4:focus-visible,h5:focus-visible,h6:focus-visible{outline:none}h1:hover .header-anchor,h2:hover .header-anchor,h3:hover .header-anchor,h4:hover .header-anchor,h5:hover .header-anchor,h6:hover .header-anchor{opacity:1}h1{font-size:2.2rem}h2{font-size:1.65rem;padding-bottom:.3rem;border-bottom:1px solid var(--c-border);transition:border-color var(--t-color)}h3{font-size:1.35rem}h4{font-size:1.15rem}h5{font-size:1.05rem}h6{font-size:1rem}a.header-anchor{font-size:.85em;float:left;margin-left:-.87em;padding-right:.23em;margin-top:.125em;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media print{a.header-anchor{display:none}}a.header-anchor:hover{text-decoration:none}a.header-anchor:focus-visible{opacity:1}@media print{a[href^="http://"]:after,a[href^="https://"]:after{content:" (" attr(href) ") "}}p,ul,ol{line-height:1.7;overflow-wrap:break-word}hr{border:0;border-top:1px solid var(--c-border)}table{border-collapse:collapse;margin:1rem 0;display:block;overflow-x:auto;transition:border-color var(--t-color)}tr{border-top:1px solid var(--c-border-dark);transition:border-color var(--t-color)}tr:nth-child(2n){background-color:var(--c-bg-light);transition:background-color var(--t-color)}tr:nth-child(2n) code{background-color:var(--c-bg-dark)}th,td{padding:.6em 1em;border:1px solid var(--c-border-dark);transition:border-color var(--t-color)}.arrow{display:inline-block;width:0;height:0}.arrow.up{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:6px solid var(--c-bg-arrow)}.arrow.down{border-left:4px solid transparent;border-right:4px solid transparent;border-top:6px solid var(--c-bg-arrow)}.arrow.right{border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--c-bg-arrow)}.arrow.left{border-top:4px solid transparent;border-bottom:4px solid transparent;border-right:6px solid var(--c-bg-arrow)}.badge{display:inline-block;font-size:14px;font-weight:600;height:18px;line-height:18px;border-radius:3px;padding:0 6px;color:var(--c-bg);vertical-align:top;transition:color var(--t-color),background-color var(--t-color)}.badge.tip{background-color:var(--c-badge-tip)}.badge.warning{background-color:var(--c-badge-warning);color:var(--c-badge-warning-text)}.badge.danger{background-color:var(--c-badge-danger);color:var(--c-badge-danger-text)}.badge+.badge{margin-left:5px}code[class*=language-],pre[class*=language-]{color:#ccc;background:none;font-family:var(--font-family-code);font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.comment,.token.block-comment,.token.prolog,.token.doctype,.token.cdata{color:#999}.token.punctuation{color:#ccc}.token.tag,.token.attr-name,.token.namespace,.token.deleted{color:#ec5975}.token.function-name{color:#6196cc}.token.boolean,.token.number,.token.function{color:#f08d49}.token.property,.token.class-name,.token.constant,.token.symbol{color:#f8c555}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin{color:#cc99cd}.token.string,.token.char,.token.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc}.token.important,.token.bold{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:#3eaf7c}.theme-default-content pre,.theme-default-content pre[class*=language-]{line-height:1.375;padding:1.3rem 1.5rem;margin:.85rem 0;border-radius:6px;overflow:auto}.theme-default-content pre code,.theme-default-content pre[class*=language-] code{color:#fff;padding:0;background-color:transparent!important;border-radius:0;overflow-wrap:unset;-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.theme-default-content .line-number{font-family:var(--font-family-code)}div[class*=language-]{position:relative;background-color:var(--code-bg-color);border-radius:6px}div[class*=language-]:before{content:attr(data-ext);position:absolute;z-index:3;top:.8em;right:1em;font-size:.75rem;color:var(--code-ln-color)}div[class*=language-] pre,div[class*=language-] pre[class*=language-]{background:transparent!important;position:relative;z-index:1}div[class*=language-] .highlight-lines{-webkit-user-select:none;-moz-user-select:none;user-select:none;padding-top:1.3rem;position:absolute;top:0;left:0;width:100%;line-height:1.375}div[class*=language-] .highlight-lines .highlight-line{background-color:var(--code-hl-bg-color)}div[class*=language-]:not(.line-numbers-mode) .line-numbers{display:none}div[class*=language-].line-numbers-mode .highlight-lines .highlight-line{position:relative}div[class*=language-].line-numbers-mode .highlight-lines .highlight-line:before{content:" ";position:absolute;z-index:2;left:0;top:0;display:block;width:var(--code-ln-wrapper-width);height:100%}div[class*=language-].line-numbers-mode pre{margin-left:var(--code-ln-wrapper-width);padding-left:1rem;vertical-align:middle}div[class*=language-].line-numbers-mode .line-numbers{position:absolute;top:0;width:var(--code-ln-wrapper-width);text-align:center;color:var(--code-ln-color);padding-top:1.25rem;line-height:1.375;counter-reset:line-number}div[class*=language-].line-numbers-mode .line-numbers .line-number{position:relative;z-index:3;-webkit-user-select:none;-moz-user-select:none;user-select:none;height:1.375em}div[class*=language-].line-numbers-mode .line-numbers .line-number:before{counter-increment:line-number;content:counter(line-number);font-size:.85em}div[class*=language-].line-numbers-mode:after{content:"";position:absolute;top:0;left:0;width:var(--code-ln-wrapper-width);height:100%;border-radius:6px 0 0 6px;border-right:1px solid var(--code-hl-bg-color)}@media (max-width: 419px){.theme-default-content div[class*=language-]{margin:.85rem -1.5rem;border-radius:0}}.code-group__nav{margin-top:.85rem;margin-bottom:calc(-1.7rem - 6px);padding-bottom:calc(1.7rem - 6px);padding-left:10px;padding-top:10px;border-top-left-radius:6px;border-top-right-radius:6px;background-color:var(--code-bg-color)}.code-group__ul{margin:auto 0;padding-left:0;display:inline-flex;list-style:none}.code-group__nav-tab{border:0;padding:5px;cursor:pointer;background-color:transparent;font-size:.85em;line-height:1.4;color:#ffffffe6;font-weight:600}.code-group__nav-tab:focus{outline:none}.code-group__nav-tab:focus-visible{outline:1px solid rgba(255,255,255,.9)}.code-group__nav-tab-active{border-bottom:var(--c-brand) 1px solid}@media (max-width: 419px){.code-group__nav{margin-left:-1.5rem;margin-right:-1.5rem;border-radius:0}}.code-group-item{display:none}.code-group-item__active{display:block}.code-group-item>pre{background-color:orange}.custom-container{transition:color var(--t-color),border-color var(--t-color),background-color var(--t-color)}.custom-container .custom-container-title{font-weight:600}.custom-container .custom-container-title:not(:only-child){margin-bottom:-.4rem}.custom-container.tip,.custom-container.warning,.custom-container.danger{padding:.1rem 1.5rem;border-left-width:.5rem;border-left-style:solid;margin:1rem 0}.custom-container.tip{border-color:var(--c-tip);background-color:var(--c-tip-bg);color:var(--c-tip-text)}.custom-container.tip .custom-container-title{color:var(--c-tip-title)}.custom-container.tip a{color:var(--c-tip-text-accent)}.custom-container.tip code{background-color:var(--c-bg-dark)}.custom-container.warning{border-color:var(--c-warning);background-color:var(--c-warning-bg);color:var(--c-warning-text)}.custom-container.warning .custom-container-title{color:var(--c-warning-title)}.custom-container.warning a{color:var(--c-warning-text-accent)}.custom-container.warning blockquote{border-left-color:var(--c-warning-border-dark);color:var(--c-warning-text-quote)}.custom-container.warning code{color:var(--c-warning-text-light);background-color:var(--c-warning-bg-light)}.custom-container.warning details{background-color:var(--c-warning-details-bg)}.custom-container.warning details code{background-color:var(--c-warning-bg-lighter)}.custom-container.warning .external-link-icon{--external-link-icon-color: var(--c-warning-text-quote)}.custom-container.danger{border-color:var(--c-danger);background-color:var(--c-danger-bg);color:var(--c-danger-text)}.custom-container.danger .custom-container-title{color:var(--c-danger-title)}.custom-container.danger a{color:var(--c-danger-text-accent)}.custom-container.danger blockquote{border-left-color:var(--c-danger-border-dark);color:var(--c-danger-text-quote)}.custom-container.danger code{color:var(--c-danger-text-light);background-color:var(--c-danger-bg-light)}.custom-container.danger details{background-color:var(--c-danger-details-bg)}.custom-container.danger details code{background-color:var(--c-danger-bg-lighter)}.custom-container.danger .external-link-icon{--external-link-icon-color: var(--c-danger-text-quote)}.custom-container.details{display:block;position:relative;border-radius:2px;margin:1.6em 0;padding:1.6em;background-color:var(--c-details-bg)}.custom-container.details code{background-color:var(--c-bg-darker)}.custom-container.details h4{margin-top:0}.custom-container.details figure:last-child,.custom-container.details p:last-child{margin-bottom:0;padding-bottom:0}.custom-container.details summary{outline:none;cursor:pointer}.home{padding:var(--navbar-height) 2rem 0;max-width:var(--homepage-width);margin:0 auto;display:block}.home .hero{text-align:center}.home .hero img{max-width:100%;max-height:280px;display:block;margin:3rem auto 1.5rem}.home .hero h1{font-size:3rem}.home .hero h1,.home .hero .description,.home .hero .actions{margin:1.8rem auto}.home .hero .actions{display:flex;flex-wrap:wrap;gap:1rem;justify-content:center}.home .hero .description{max-width:35rem;font-size:1.6rem;line-height:1.3;color:var(--c-text-lightest)}.home .hero .action-button{display:inline-block;font-size:1.2rem;padding:.8rem 1.6rem;border-width:2px;border-style:solid;border-radius:4px;transition:background-color var(--t-color);box-sizing:border-box}.home .hero .action-button.primary{color:var(--c-bg);background-color:var(--c-brand);border-color:var(--c-brand)}.home .hero .action-button.primary:hover{background-color:var(--c-brand-light)}.home .hero .action-button.secondary{color:var(--c-brand);background-color:var(--c-bg);border-color:var(--c-brand)}.home .hero .action-button.secondary:hover{color:var(--c-bg);background-color:var(--c-brand-light)}.home .features{border-top:1px solid var(--c-border);transition:border-color var(--t-color);padding:1.2rem 0;margin-top:2.5rem;display:flex;flex-wrap:wrap;align-items:flex-start;align-content:stretch;justify-content:space-between}.home .feature{flex-grow:1;flex-basis:30%;max-width:30%}.home .feature h2{font-size:1.4rem;font-weight:500;border-bottom:none;padding-bottom:0;color:var(--c-text-light)}.home .feature p{color:var(--c-text-lighter)}.home .theme-default-content{padding:0;margin:0}.home .footer{padding:2.5rem;border-top:1px solid var(--c-border);text-align:center;color:var(--c-text-lighter);transition:border-color var(--t-color)}@media (max-width: 719px){.home .features{flex-direction:column}.home .feature{max-width:100%;padding:0 2.5rem}}@media (max-width: 419px){.home{padding-left:1.5rem;padding-right:1.5rem}.home .hero img{max-height:210px;margin:2rem auto 1.2rem}.home .hero h1{font-size:2rem}.home .hero h1,.home .hero .description,.home .hero .actions{margin:1.2rem auto}.home .hero .description{font-size:1.2rem}.home .hero .action-button{font-size:1rem;padding:.6rem 1.2rem}.home .feature h2{font-size:1.25rem}}.page{padding-top:var(--navbar-height);padding-left:var(--sidebar-width)}.navbar{position:fixed;z-index:20;top:0;left:0;right:0;height:var(--navbar-height);box-sizing:border-box;border-bottom:1px solid var(--c-border);background-color:var(--c-bg-navbar);transition:background-color var(--t-color),border-color var(--t-color)}.sidebar{font-size:16px;width:var(--sidebar-width);position:fixed;z-index:10;margin:0;top:var(--navbar-height);left:0;bottom:0;box-sizing:border-box;border-right:1px solid var(--c-border);overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--c-brand) var(--c-border);background-color:var(--c-bg-sidebar);transition:transform var(--t-transform),background-color var(--t-color),border-color var(--t-color)}.sidebar::-webkit-scrollbar{width:7px}.sidebar::-webkit-scrollbar-track{background-color:var(--c-border)}.sidebar::-webkit-scrollbar-thumb{background-color:var(--c-brand)}.sidebar-mask{position:fixed;z-index:9;top:0;left:0;width:100vw;height:100vh;display:none}.theme-container.sidebar-open .sidebar-mask{display:block}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(1){transform:rotate(45deg) translate3d(5.5px,5.5px,0)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(2){transform:scale3d(0,1,1)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(3){transform:rotate(-45deg) translate3d(6px,-6px,0)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(1),.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(3){transform-origin:center}.theme-container.no-navbar .theme-default-content h1,.theme-container.no-navbar .theme-default-content h2,.theme-container.no-navbar .theme-default-content h3,.theme-container.no-navbar .theme-default-content h4,.theme-container.no-navbar .theme-default-content h5,.theme-container.no-navbar .theme-default-content h6{margin-top:1.5rem;padding-top:0}.theme-container.no-navbar .page{padding-top:0}.theme-container.no-navbar .sidebar{top:0}.theme-container.no-sidebar .sidebar{display:none}@media (max-width: 719px){.theme-container.no-sidebar .sidebar{display:block}}.theme-container.no-sidebar .page{padding-left:0}.theme-default-content a:hover{text-decoration:underline}.theme-default-content img{max-width:100%}.theme-default-content h1,.theme-default-content h2,.theme-default-content h3,.theme-default-content h4,.theme-default-content h5,.theme-default-content h6{margin-top:calc(.5rem - var(--navbar-height));padding-top:calc(1rem + var(--navbar-height));margin-bottom:0}.theme-default-content h1:first-child,.theme-default-content h2:first-child,.theme-default-content h3:first-child,.theme-default-content h4:first-child,.theme-default-content h5:first-child,.theme-default-content h6:first-child{margin-bottom:1rem}.theme-default-content h1:first-child+p,.theme-default-content h1:first-child+pre,.theme-default-content h1:first-child+.custom-container,.theme-default-content h2:first-child+p,.theme-default-content h2:first-child+pre,.theme-default-content h2:first-child+.custom-container,.theme-default-content h3:first-child+p,.theme-default-content h3:first-child+pre,.theme-default-content h3:first-child+.custom-container,.theme-default-content h4:first-child+p,.theme-default-content h4:first-child+pre,.theme-default-content h4:first-child+.custom-container,.theme-default-content h5:first-child+p,.theme-default-content h5:first-child+pre,.theme-default-content h5:first-child+.custom-container,.theme-default-content h6:first-child+p,.theme-default-content h6:first-child+pre,.theme-default-content h6:first-child+.custom-container{margin-top:2rem}@media (max-width: 959px){.sidebar{font-size:15px;width:var(--sidebar-width-mobile)}.page{padding-left:var(--sidebar-width-mobile)}}@media (max-width: 719px){.sidebar{top:0;padding-top:var(--navbar-height);transform:translate(-100%)}.page{padding-left:0}.theme-container.sidebar-open .sidebar{transform:translate(0)}.theme-container.no-navbar .sidebar{padding-top:0}}@media (max-width: 419px){h1{font-size:1.9rem}}.navbar{--navbar-line-height: calc( var(--navbar-height) - 2 * var(--navbar-padding-v) );padding:var(--navbar-padding-v) var(--navbar-padding-h);line-height:var(--navbar-line-height)}.navbar .logo{height:var(--navbar-line-height);margin-right:var(--navbar-padding-v);vertical-align:top}.navbar .site-name{font-size:1.3rem;font-weight:600;color:var(--c-text);position:relative}.navbar .navbar-items-wrapper{display:flex;position:absolute;box-sizing:border-box;top:var(--navbar-padding-v);right:var(--navbar-padding-h);height:var(--navbar-line-height);padding-left:var(--navbar-padding-h);white-space:nowrap;font-size:.9rem}.navbar .navbar-items-wrapper .search-box{flex:0 0 auto;vertical-align:top}@media screen and (max-width: 719px){.navbar{padding-left:4rem}.navbar .can-hide{display:none}.navbar .site-name{width:calc(100vw - 9.4rem);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}}.navbar-items{display:inline-block}@media print{.navbar-items{display:none}}.navbar-items a{display:inline-block;line-height:1.4rem;color:inherit}.navbar-items a:hover,.navbar-items a.router-link-active{color:var(--c-text)}.navbar-items .navbar-item{position:relative;display:inline-block;margin-left:1.5rem;line-height:var(--navbar-line-height)}.navbar-items .navbar-item:first-child{margin-left:0}.navbar-items .navbar-item>a:hover,.navbar-items .navbar-item>a.router-link-active{margin-bottom:-2px;border-bottom:2px solid var(--c-text-accent)}@media (max-width: 719px){.navbar-items .navbar-item{margin-left:0}.navbar-items .navbar-item>a:hover,.navbar-items .navbar-item>a.router-link-active{margin-bottom:0;border-bottom:none}.navbar-items a:hover,.navbar-items a.router-link-active{color:var(--c-text-accent)}}.toggle-sidebar-button{position:absolute;top:.6rem;left:1rem;display:none;padding:.6rem;cursor:pointer}.toggle-sidebar-button .icon{display:flex;flex-direction:column;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;cursor:inherit}.toggle-sidebar-button .icon span{display:inline-block;width:100%;height:2px;border-radius:2px;background-color:var(--c-text);transition:transform var(--t-transform)}.toggle-sidebar-button .icon span:nth-child(2){margin:6px 0}@media screen and (max-width: 719px){.toggle-sidebar-button{display:block}}.toggle-color-mode-button{display:flex;margin:auto;margin-left:1rem;border:0;background:none;color:var(--c-text);opacity:.8;cursor:pointer}@media print{.toggle-color-mode-button{display:none}}.toggle-color-mode-button:hover{opacity:1}.toggle-color-mode-button .icon{width:1.25rem;height:1.25rem}.DocSearch{transition:background-color var(--t-color)}.navbar-dropdown-wrapper{cursor:pointer}.navbar-dropdown-wrapper .navbar-dropdown-title,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile{display:block;font-size:.9rem;font-family:inherit;cursor:inherit;padding:inherit;line-height:1.4rem;background:transparent;border:none;font-weight:500;color:var(--c-text)}.navbar-dropdown-wrapper .navbar-dropdown-title:hover,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile:hover{border-color:transparent}.navbar-dropdown-wrapper .navbar-dropdown-title .arrow,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile .arrow{vertical-align:middle;margin-top:-1px;margin-left:.4rem}.navbar-dropdown-wrapper .navbar-dropdown-title-mobile{display:none;font-weight:600;font-size:inherit}.navbar-dropdown-wrapper .navbar-dropdown-title-mobile:hover{color:var(--c-text-accent)}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item{color:inherit;line-height:1.7rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle{margin:.45rem 0 0;border-top:1px solid var(--c-border);padding:1rem 0 .45rem;font-size:.9rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>span{padding:0 1.5rem 0 1.25rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>a{font-weight:inherit}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>a.router-link-active:after{display:none}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem-wrapper{padding:0;list-style:none}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem-wrapper .navbar-dropdown-subitem{font-size:.9em}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a{display:block;line-height:1.7rem;position:relative;border-bottom:none;font-weight:400;margin-bottom:0;padding:0 1.5rem 0 1.25rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a:hover,.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active{color:var(--c-text-accent)}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active:after{content:"";width:0;height:0;border-left:5px solid var(--c-text-accent);border-top:3px solid transparent;border-bottom:3px solid transparent;position:absolute;top:calc(50% - 2px);left:9px}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item:first-child .navbar-dropdown-subtitle{margin-top:0;padding-top:0;border-top:0}.navbar-dropdown-wrapper.mobile.open .navbar-dropdown-title,.navbar-dropdown-wrapper.mobile.open .navbar-dropdown-title-mobile{margin-bottom:.5rem}.navbar-dropdown-wrapper.mobile .navbar-dropdown-title,.navbar-dropdown-wrapper.mobile .navbar-dropdown-title-mobile{display:none}.navbar-dropdown-wrapper.mobile .navbar-dropdown-title-mobile{display:block}.navbar-dropdown-wrapper.mobile .navbar-dropdown{transition:height .1s ease-out;overflow:hidden}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle{border-top:0;margin-top:0;padding-top:0;padding-bottom:0}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle,.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item>a{font-size:15px;line-height:2rem}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem{font-size:14px;padding-left:1rem}.navbar-dropdown-wrapper:not(.mobile){height:1.8rem}.navbar-dropdown-wrapper:not(.mobile):hover .navbar-dropdown,.navbar-dropdown-wrapper:not(.mobile).open .navbar-dropdown{display:block!important}.navbar-dropdown-wrapper:not(.mobile).open:blur{display:none}.navbar-dropdown-wrapper:not(.mobile) .navbar-dropdown{display:none;height:auto!important;box-sizing:border-box;max-height:calc(100vh - 2.7rem);overflow-y:auto;position:absolute;top:100%;right:0;background-color:var(--c-bg-navbar);padding:.6rem 0;border:1px solid var(--c-border);border-bottom-color:var(--c-border-dark);text-align:left;border-radius:.25rem;white-space:nowrap;margin:0}.page{padding-bottom:2rem;display:block}.page .theme-default-content{max-width:var(--content-width);margin:0 auto;padding:2rem 2.5rem;padding-top:0}@media (max-width: 959px){.page .theme-default-content{padding:2rem}}@media (max-width: 419px){.page .theme-default-content{padding:1.5rem}}.page-meta{max-width:var(--content-width);margin:0 auto;padding:1rem 2.5rem;overflow:auto}@media (max-width: 959px){.page-meta{padding:2rem}}@media (max-width: 419px){.page-meta{padding:1.5rem}}.page-meta .meta-item{cursor:default;margin-top:.8rem}.page-meta .meta-item .meta-item-label{font-weight:500;color:var(--c-text-lighter)}.page-meta .meta-item .meta-item-info{font-weight:400;color:var(--c-text-quote)}.page-meta .edit-link{display:inline-block;margin-right:.25rem}@media print{.page-meta .edit-link{display:none}}.page-meta .last-updated{float:right}@media (max-width: 719px){.page-meta .last-updated{font-size:.8em;float:none}.page-meta .contributors{font-size:.8em}}.page-nav{max-width:var(--content-width);margin:0 auto;padding:1rem 2.5rem 2rem;padding-bottom:0}@media (max-width: 959px){.page-nav{padding:2rem}}@media (max-width: 419px){.page-nav{padding:1.5rem}}.page-nav .inner{min-height:2rem;margin-top:0;border-top:1px solid var(--c-border);transition:border-color var(--t-color);padding-top:1rem;overflow:auto}.page-nav .prev a:before{content:"\2190"}.page-nav .next{float:right}.page-nav .next a:after{content:"\2192"}.sidebar ul{padding:0;margin:0;list-style-type:none}.sidebar a{display:inline-block}.sidebar .navbar-items{display:none;border-bottom:1px solid var(--c-border);transition:border-color var(--t-color);padding:.5rem 0 .75rem}.sidebar .navbar-items a{font-weight:600}.sidebar .navbar-items .navbar-item{display:block;line-height:1.25rem;font-size:1.1em;padding:.5rem 0 .5rem 1.5rem}.sidebar .sidebar-items{padding:1.5rem 0}@media (max-width: 719px){.sidebar .navbar-items{display:block}.sidebar .navbar-items .navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active:after{top:calc(1rem - 2px)}.sidebar .sidebar-items{padding:1rem 0}}.sidebar-item{cursor:default;border-left:.25rem solid transparent;color:var(--c-text)}.sidebar-item:focus-visible{outline-width:1px;outline-offset:-1px}.sidebar-item.active:not(p.sidebar-heading){font-weight:600;color:var(--c-text-accent);border-left-color:var(--c-text-accent)}.sidebar-item.sidebar-heading{transition:color .15s ease;font-size:1.1em;font-weight:700;padding:.35rem 1.5rem .35rem 1.25rem;width:100%;box-sizing:border-box;margin:0}.sidebar-item.sidebar-heading+.sidebar-item-children{transition:height .1s ease-out;overflow:hidden;margin-bottom:.75rem}.sidebar-item.collapsible{cursor:pointer}.sidebar-item.collapsible .arrow{position:relative;top:-.12em;left:.5em}.sidebar-item:not(.sidebar-heading){font-size:1em;font-weight:400;display:inline-block;margin:0;padding:.35rem 1rem .35rem 2rem;line-height:1.4;width:100%;box-sizing:border-box}.sidebar-item:not(.sidebar-heading)+.sidebar-item-children{padding-left:1rem;font-size:.95em}.sidebar-item-children .sidebar-item-children .sidebar-item:not(.sidebar-heading){padding:.25rem 1rem .25rem 1.75rem}.sidebar-item-children .sidebar-item-children .sidebar-item:not(.sidebar-heading).active{font-weight:500;border-left-color:transparent}a.sidebar-heading+.sidebar-item-children .sidebar-item:not(.sidebar-heading).active{border-left-color:transparent}a.sidebar-item{cursor:pointer}a.sidebar-item:hover{color:var(--c-text-accent)}.table-of-contents .badge{vertical-align:middle}.dropdown-enter-from,.dropdown-leave-to{height:0!important}.fade-slide-y-enter-active{transition:all .2s ease}.fade-slide-y-leave-active{transition:all .2s cubic-bezier(1,.5,.8,1)}.fade-slide-y-enter-from,.fade-slide-y-leave-to{transform:translateY(10px);opacity:0}:root{--c-brand: #99CC33;--c-brand-light: #D7DB0A;--c-text-accent: #826aed;--code-bg-color: #383838;--code-hl-bg-color: rgba(0, 0, 0, .5);--code-ln-color: #fff;--code-ln-wrapper-width: 2rem;--content-width: 850px;--homepage-width: 100%;--c-text-quote: rgb(130, 122, 165);--c-tip: #b5d935 !important;--c-details-bg: rgba(125,125,125,.05)}:root .navbar-dropdown{box-shadow:0 0 10px #14141433;border-radius:.7em}html.dark a{color:#a493f1}.sidebar-item-children .sidebar-item-children{opacity:.9;font-size:.92em!important;padding-bottom:.3em;border-left:.3em solid var(--c-text-accent)}.sidebar-item-children a{padding-top:.1em!important;padding-bottom:.1em!important}.sidebar-item-children .sidebar-item-children a:before{content:" ";margin-left:-.7em;color:#0000004d}.theme-container,blockquote{font-size:1.1rem}p,ul,ol{line-height:1.5em}code{border-radius:.4rem;background-color:#b4b4b41a!important;box-shadow:inset 0 0 10px #96969633!important;border:1px solid rgba(150,150,150,.3);padding:.1em .3em!important;font-size:.85rem}pre code{background:none!important;box-shadow:none!important;border:none!important}a code{color:var(--c-text-accent)!important}::-moz-selection{color:#000;background:#a3d03d}::selection{color:#000;background:#a3d03d}.search-box{--search-border-color: rgba(138, 125, 189, .5)}img{border-radius:.5em}.tip{border-left-width:.25em!important;padding:.2em .2em .5em 2.4em!important;max-width:700px}details,.custom-container{border-radius:10px!important;border:2px solid rgba(255,255,255,.05)!important;outline:1px solid rgba(0,0,0,.2);box-shadow:inset 0 0 20px #0000000d!important;padding:1em!important;margin:.5em 0!important}details summary{font-weight:700!important}details p{margin-top:.5em}.custom-container p:not(:first){margin:0}.custom-container-title{padding-bottom:.5em}table{box-shadow:inset 0 0 40px #6464641a!important;border-radius:.5em;padding:.5em .5em .5em 1em;min-width:100%}@media screen and (min-width: 1700px){table{min-width:1000px}}@media screen and (min-width: 1900px){table{min-width:1200px}}td{width:20%}td:first-child{min-width:10%}td:last-child{width:30%}th{text-align:left}tr,td,th{border:none!important;padding:.5em 1em .8em 0!important}tr,td,th{font-size:.9em!important;background:none!important}tr{border-bottom:1px solid rgba(155,155,155,.2)!important;display:table-row}tr:last-child{border-bottom:none!important}td{vertical-align:top}.sample-code-links{display:flex;margin-top:-15px;top:0px;border-radius:0 0 10px 10px;width:calc(100% - .6em);padding:.4em .1em .15em .6em}.action[data-v-1f05b024]{display:inline-block;background:rgba(125,125,125,.1);color:inherit;padding:1em 1em 1.2em;border-radius:1em;margin:.5em;box-shadow:0 0 .5em #64646433,inset 0 0 1.5em #6464641a;border:1px solid rgba(100,100,100,.3);transition:transform .2s ease-in-out;font-size:1.2em}a[data-v-1f05b024]:hover{text-decoration:none}.action[data-v-1f05b024]:hover{transform:scale(1.1);transition:transform .1s ease-in-out}.actiongroup{display:flex;align-items:center;justify-content:center;flex-wrap:wrap}.actiongroup>*{margin:0 .5em}.contribution[data-v-543822ed]{margin-bottom:10px}.header[data-v-543822ed]{display:flex;align-items:center;justify-content:space-between;border-radius:1em;padding-right:1em;-webkit-user-select:none;-moz-user-select:none;user-select:none}.header.gradient[data-v-543822ed]{background:linear-gradient(180deg,rgba(100,100,200,0) 0%,rgba(100,100,200,.1) 3%,rgba(200,200,200,0) 100%)}.profile[data-v-543822ed]{display:flex;align-items:center;margin-bottom:1em;margin-left:.9em;margin-top:1em}.profile img[data-v-543822ed]{width:50px;height:50px;margin-right:10px;border-radius:50%;border:2px solid #ccc;pointer-events:none}.profile .authorname[data-v-543822ed]{font-size:1.2rem;font-weight:600;color:#000;text-decoration:none}html.dark .authorname[data-v-543822ed]{color:#fff}.entry[data-v-bcc3d6f6]{display:block;padding:.2em .2em .3em 1em;border-radius:.7em;font-size:1.4em;font-weight:400}.entry[data-v-bcc3d6f6]:hover{text-decoration:none;background-color:#6464de33;box-shadow:inset 0 0 50px #fff}html.dark .entry[data-v-bcc3d6f6]:hover{box-shadow:inset 0 0 30px #323232}a[data-v-f801cb6e]{color:initial;text-decoration:none}.preview[data-v-f801cb6e]{margin:1%;display:inline-block;position:relative;min-width:48%;flex-grow:1;flex-shrink:1;height:300px;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none;border-radius:.5em;box-shadow:0 0 1px #00006433;border:1px solid rgba(0,0,100,.3);transition:all .1s ease-in-out}.preview[data-v-f801cb6e]:hover{cursor:pointer;box-shadow:0 0 10px #0000641a;transition:all .1s ease-in-out}.content[data-v-f801cb6e]{overflow:hidden;bottom:10px;padding:0 10px;position:relative}.title[data-v-f801cb6e]{display:flex;align-items:center;font-size:1.5em;margin:0;padding:1em;height:3em;background:linear-gradient(180deg,rgba(100,100,200,.2) 20%,rgba(200,200,200,0) 100%);-webkit-user-select:initial!important;-moz-user-select:initial!important;user-select:initial!important}.overview-link{display:block;padding-top:1em;padding-bottom:1em;text-decoration:none}.overview-link:hover{text-decoration:none}.previews{display:flex;flex-direction:row;flex-wrap:wrap}.footer{margin-top:1em;display:block;padding-top:1em;border-top:1px solid rgba(100,100,100,.2)}.list{display:flex;flex-direction:column}.list>*{width:95%;margin-bottom:10px;border:1px solid rgba(100,100,200,.2);border-radius:.5em;padding:.5em .5em 2em;background:linear-gradient(180deg,rgba(100,100,200,.1) 0%,rgba(200,200,200,.1) 50%)}html.dark .list>*{border:1px solid rgba(200,200,200,.05);background:linear-gradient(180deg,rgba(100,100,200,.1) 0%,rgba(100,100,100,.2) 50%)}.footer[data-v-c089a4fb]{display:flex;width:100%;justify-content:center;align-items:center;padding:1em;font-size:.8em;opacity:.8}a[data-v-c089a4fb]{color:inherit;font-weight:400;margin-left:5px}button[data-v-475bbd8b]{border:none;border-radius:10em;text-shadow:none;padding:1em 2em;background-color:var(--bdbb8492);transition:background-color .2s ease-in-out;cursor:pointer;margin:.2em .2em .2em -.3em;color:#fff;font-size:var(--8142adbe)}button[data-v-475bbd8b]:hover{background-color:#6248be;background-color:var(--54718738);transition:background-color .2s ease-in-out}a[data-v-475bbd8b]{text-shadow:none!important}.quotes[data-v-96af3692]{align-items:center;justify-content:center;flex-wrap:wrap;padding:0 .5em .5em;text-align:center;min-height:90px;display:none}@media (max-width: 768px){.quotes[data-v-96af3692]{min-height:200px}}.quotes[data-v-96af3692]{font-size:1.2em;font-style:italic}div[data-v-916978fd]{margin-top:.3em;display:flex;flex-direction:column}iframe[data-v-916978fd]{width:100%;height:100%;border:0;aspect-ratio:16/9}iframe[data-v-916978fd]:only-of-type{border-radius:1em}iframe[data-v-916978fd]:first-of-type{border-top-left-radius:1em;border-top-right-radius:1em;margin-bottom:.1em}iframe[data-v-916978fd]:last-of-type{border-bottom-left-radius:1em;border-bottom-right-radius:1em;margin-top:.1em}@media (max-aspect-ratio: 1/1){iframe[data-v-916978fd]{aspect-ratio:9/9}}@media (max-aspect-ratio: 9/16){iframe[data-v-916978fd]{aspect-ratio:9/14}}button{background-color:#1374ef;border:none;color:#fff;padding:.3em;border-radius:.5em;margin-top:.5em}button:hover{background-color:#277ee9;cursor:pointer}.code{display:none}.root[data-v-47fef520]{display:flex}.testimonial[data-v-47fef520]{display:inline-block;margin:0 0 1rem;padding:1rem;background-color:#7d7d7d1a;border-radius:1em;border:1px solid rgba(125,125,125,.2);box-shadow:inset 0 0 10px #7d7d7d1a}.marker[data-v-47fef520]{position:absolute;font-size:2rem;color:#32323280;margin:-1.2rem -.7rem}.container[data-v-144c2fbe]{max-width:100%;height:var(--5159d46b);max-height:var(--4cc05253)}video[data-v-144c2fbe],#ytplayer[data-v-144c2fbe]{background:rgba(0,0,0,.2);display:block;width:var(--49dcf46c);height:var(--25a42c34);max-width:100%;max-height:100%;margin:.75em 0;max-height:var(--02f9f579)}#ytplayer[data-v-144c2fbe]{aspect-ratio:16/9;border-radius:1em}@media screen and (max-width: 1200px){.container[data-v-144c2fbe]{width:100%;height:auto}video[data-v-144c2fbe],#ytplayer[data-v-144c2fbe]{width:100%;height:auto}}/*! @docsearch/css 3.6.0 | MIT License | ยฉ Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 1px 0 rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 1px 1px 0 rgba(3,4,9,.30196078431372547);--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;position:relative;padding:0 0 2px;border:0;top:-1px;width:20px}.DocSearch-Button-Key--pressed{transform:translate3d(0,1px,0);box-shadow:var(--docsearch-key-pressed-shadow)}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::-moz-placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"\bb "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:2px;box-shadow:var(--docsearch-key-shadow);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;color:var(--docsearch-muted-color);border:0;width:20px}.DocSearch-VisuallyHiddenForAccessibility{clip:rect(0 0 0 0);-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@media (min-width: 751px){#docsearch-container{min-width:171.36px}}@media (max-width: 750px){.DocSearch-Container{position:fixed}#docsearch-container{min-width:52px}}@media print{#docsearch-container{display:none}}
diff --git a/assets/style-e9220a04.js b/assets/style-e9220a04.js
new file mode 100644
index 000000000..c93054809
--- /dev/null
+++ b/assets/style-e9220a04.js
@@ -0,0 +1 @@
+const t="";export{t as default};
diff --git a/assets/technical-overview.html-2f5c2d77.js b/assets/technical-overview.html-2f5c2d77.js
new file mode 100644
index 000000000..a69ea7632
--- /dev/null
+++ b/assets/technical-overview.html-2f5c2d77.js
@@ -0,0 +1,264 @@
+import{_ as i,M as o,p as r,q as l,R as s,t as n,N as a,V as c,a1 as t}from"./framework-c782e227.js";const u={},d=t('
a number of components and tools that allow you to set up scenes for Needle Engine from e.g. the Unity Editor.
an exporter that turns scene and component data into glTF.
a web runtime that loads and runs the produced glTF files and their extensions.
The editor extensions currently support the Unity Editor, with some promising experiments for Blender on the horizon (but no ETA). The web runtime uses three.js for rendering, adds a component system on top of the three scene graph and hooks up extension loaders for our custom glTF extensions.
Effectively, this turns the Unity Editor into a full member of a regular web development toolchain โ "just" one more piece that gets added to the regular HTML, JavaScript, CSS and bundling workflow.
Note: Materials using these extensions can be exported from Unity via UnityGLTF's PBRGraph material.
Note: Audio and variants are already supported in Needle Engine through NEEDLE_components and NEEDLE_persistent_assets, but there are some options for more alignment to existing proposals such as KHR_audio and KHR_materials_variants.
Needle Engine stores custom data in glTF files through our vendor extensions. These extensions are designed to be flexible and allow relatively arbitrary data to put into them. Notably, no code is stored in these files. Interactive components is restored from the data at runtime. This has some similarities to how AssetBundles function in Unity โ the receiving side of an asset needs to have matching code for components stored in the file.
We're currently not prodiving schemas for these extensions as they are still in development. The JSON snippets below demonstrates extension usage by example and includes notes on architectural choices and what we may change in future releases.
References between pieces of data are currently constructed through a mix of indices into other parts of the glTF file and JSON pointers. We may consolidate these approaches in a future release. We're also storing string-based GUIDs for cases where ordering is otherwise hard to resolve (e.g. two components referencing each other) that we may remove in the future.
This extension contains per-node component data. The component names map to type names on both the JavaScript and C# side. Multiple components with the same name can be added to the same node.
Note: Storing only the component type name means that type names currently need to be unique per project. We're planning to include package names in a future release to loosen this constraint to unique component type names per package instead of globally.
Note: Currently there's no versioning information in the extension (which npm packaage does a component belong to, which version of that package was it exported against). We're planning to include versioning information in a future release.
Note: Currently all components are in the builtin_components array. We might rename this to just components in a future release.
Components in NEEDLE_components can reference data via JSON Pointers. The data in NEEDLE_persistent_assets is often referenced multiple times by different components and is thus stored separately in a root extension. By design, they are always referenced by something else (or have references within each other), and thus do not store type information at all: they're simply pieces of JSON data and components referencing them currently need to know what they expect.
Examples for assets/data stored in here are:
AnimatorControllers, their layers and states
PlayableAssets (timelines), their tracks and embedded clips
SignalAssets
...
Data in persistent_assets can reference other persistent_assets via JSON Pointer, but by design can't reference NEEDLE_components. This is similar to the separation beween "Scene data" and "AssetDatabase content" in Unity.
While Unity's compilation process from C# to IL to C++ (via IL2CPP) to WASM (via emscripten) is ingenious, it's also relatively slow. Building even a simple project to WASM takes many minutes, and that process is pretty much repeated on every code change. Some of it can be avoided through clever caching and ensuring that dev builds don't try to strip as much code, but it still stays slow.
We do have a prototype for some WASM translation, but it's far from complete and the iteration speed stays slow, so we are not actively investigating this path right now.
When looking into modern web workflows, we found that code reload times during development are neglectible, usually in sub-second ranges. This of course trades some performance (interpretation of JavaScript on the fly instead of compiler optimization at build time) for flexibility, but browsers got really good at getting the most out of JavaScript.
We believe that for iteration and tight testing workflows, it's beneficial to be able to test on device and on the target platform (the browser, in this case) as quickly and as often as possible - which is why we're skipping Unity's entire play mode, effectively always running in the browser.
Note: A really nice side effect is avoiding the entire slow "domain reload" step that usually costs 15-60 seconds each time you enter Play Mode. You're just "live" in the browser the moment you press Play.
`,13);function C(O,P){const e=o("ExternalLinkIcon"),p=o("RouterLink");return r(),l("div",null,[d,s("p",null,[n("Models, textures, animations, lights, cameras and more are stored as "),s("a",k,[n("glTF 2.0 files"),a(e)]),n(" in Needle Engine."),m,n(" Custom data is stored in "),v,n(". These cover everything from interactive components to physics, sequencing and lightmaps.")]),b,s("p",null,[n("More extensions and custom extensions can be added using the export callbacks of UnityGLTF (not documented yet) and the "),s("a",q,[n("glTF import extensions"),a(e)]),n(" of three.js.")]),h,s("p",null,[s("a",g,[n("Learn more about GLTF loading in three.js"),a(e)])]),y,s("p",null,[n("For production, we compress glTF assets with "),s("a",f,[_,a(e)]),n(". Textures use either "),x,n(", "),E,n(", "),w,n(" or no compression, depending on texture type. Meshes use "),T,n(" by default but can be configured to use "),N,n(" (per glTF file). Custom extensions are passed through in an opaque way.")]),s("p",null,[n("See the "),a(p,{to:"/deployment.html#optimization-and-compression-options"},{default:c(()=>[n("deployment & compression")]),_:1}),n(" page for more information")]),L,s("p",null,[n("Data in "),j,n(" can be animated via the currently not ratified "),s("a",R,[D,a(e)]),n(" extension.")]),S,s("p",null,[n("This extension contains additional per-node data related to state, layers, and tags. Layers are used for both rendering and physics, similar to how "),s("a",F,[n("three.js"),a(e)]),n(" and "),s("a",M,[n("Unity"),a(e)]),n(" treat them.")]),W,s("p",null,[n("This extension builds upon the archived "),s("a",H,[K,a(e)]),n(" extension and extends it in a few crucial places. While the original extension was specified against WebGL 1.0, we're using it with WebGL 2.0 here and have added a number of uniform types.")]),A])}const G=i(u,[["render",C],["__file","technical-overview.html.vue"]]);export{G as default};
diff --git a/assets/technical-overview.html-9b3121a1.js b/assets/technical-overview.html-9b3121a1.js
new file mode 100644
index 000000000..21487ec93
--- /dev/null
+++ b/assets/technical-overview.html-9b3121a1.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-3b00a577","path":"/technical-overview.html","title":"Technical Overview","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/technical overview.png"}],["meta",{"name":"og:description","content":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."}]],"description":"Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally. Needle Exporter for Unity bridges the Unity Editor and the web runtime. It helps you to export your assets, animations, lightmaps and so on to the web. It is built around the glTF standard for 3D assets."},"headers":[{"level":2,"title":"How it works","slug":"how-it-works","link":"#how-it-works","children":[]},{"level":2,"title":"glTF Assets","slug":"gltf-assets","link":"#gltf-assets","children":[{"level":3,"title":"Supported glTF extensions","slug":"supported-gltf-extensions","link":"#supported-gltf-extensions","children":[]},{"level":3,"title":"Compression","slug":"compression","link":"#compression","children":[]}]},{"level":2,"title":"Vendor-specific glTF Extensions (NEEDLE_*)","slug":"vendor-specific-gltf-extensions-needle","link":"#vendor-specific-gltf-extensions-needle","children":[{"level":3,"title":"NEEDLE_components","slug":"needle-components","link":"#needle-components","children":[]},{"level":3,"title":"NEEDLE_gameobject_data","slug":"needle-gameobject-data","link":"#needle-gameobject-data","children":[]},{"level":3,"title":"NEEDLE_lighting_settings","slug":"needle-lighting-settings","link":"#needle-lighting-settings","children":[]},{"level":3,"title":"NEEDLE_lightmaps","slug":"needle-lightmaps","link":"#needle-lightmaps","children":[]},{"level":3,"title":"NEEDLE_persistent_assets","slug":"needle-persistent-assets","link":"#needle-persistent-assets","children":[]},{"level":3,"title":"NEEDLE_techniques_webgl","slug":"needle-techniques-webgl","link":"#needle-techniques-webgl","children":[]}]},{"level":2,"title":"TypeScript and Data Mapping","slug":"typescript-and-data-mapping","link":"#typescript-and-data-mapping","children":[]},{"level":2,"title":"Rendering with three.js","slug":"rendering-with-three.js","link":"#rendering-with-three.js","children":[]},{"level":2,"title":"Why aren't you compiling to WebAssembly?","slug":"why-aren-t-you-compiling-to-webassembly","link":"#why-aren-t-you-compiling-to-webassembly","children":[]}],"git":{},"filePathRelative":"technical-overview.md"}`);export{e as data};
diff --git a/assets/testimonial-9323d462.js b/assets/testimonial-9323d462.js
new file mode 100644
index 000000000..518e53abe
--- /dev/null
+++ b/assets/testimonial-9323d462.js
@@ -0,0 +1 @@
+import{_ as a,p as o,q as n,R as e,Q as _,s as c,t as r,v as i}from"./framework-c782e227.js";const d={props:{name:String,src:String}},l={class:"root"},f={class:"testimonial"},p={class:"quote"},m={class:"name"},u=["href"];function h(t,v,s,g,k,x){return o(),n("div",l,[e("div",f,[_(' โ '),e("span",p,[c(t.$slots,"default",{},void 0,!0)]),e("span",m,[r(" โ "),e("a",{href:s.src,target:"_blank"},i(s.name),9,u)])])])}const y=a(d,[["render",h],["__scopeId","data-v-47fef520"],["__file","testimonial.vue"]]);export{y as default};
diff --git a/assets/testimonials.html-01f34c32.js b/assets/testimonials.html-01f34c32.js
new file mode 100644
index 000000000..3e0f49434
--- /dev/null
+++ b/assets/testimonials.html-01f34c32.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-7e348068","path":"/testimonials.html","title":"Testimonials ๐ฌ","lang":"en-US","frontmatter":{"next":"getting-started","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/testimonials.png"}],["meta",{"name":"og:description","content":"---\\nThis is the best thing I have seen after cinemachine in unity. Unity should acquire this"}]],"description":"---\\nThis is the best thing I have seen after cinemachine in unity. Unity should acquire this"},"headers":[],"git":{},"filePathRelative":"testimonials.md"}');export{e as data};
diff --git a/assets/testimonials.html-bd9044e7.js b/assets/testimonials.html-bd9044e7.js
new file mode 100644
index 000000000..a22bf4b1e
--- /dev/null
+++ b/assets/testimonials.html-bd9044e7.js
@@ -0,0 +1 @@
+import{_ as n,M as o,p as r,q as l,N as s,V as a,t as e,R as i}from"./framework-c782e227.js";const h={},c=i("h1",{id:"testimonials",tabindex:"-1"},[i("a",{class:"header-anchor",href:"#testimonials","aria-hidden":"true"},"#"),e(" Testimonials ๐ฌ")],-1),m=i("p",null,null,-1);function d(u,p){const t=o("testimonial");return r(),l("div",null,[c,m,s(t,{name:"Rinesh Thomas",src:"https://twitter.com/rineshthomas/status/1566342798063947777?t=z6sG3Z7mol-NfIRfTTKqCQ&s=19"},{default:a(()=>[e(" This is the best thing I have seen after cinemachine in unity. Unity should acquire this ")]),_:1}),s(t,{name:"Chris Mahoney",src:"https://twitter.com/mahoneymatic/status/1562981022932684800?t=qNqojoZkk2CZrJa7dGzqng&s=19"},{default:a(()=>[e(" Unbelievable Unity editor integration by an order of magnitude, and as straightforward as the docs claim. Wow. ")]),_:1}),s(t,{name:"Kevin Curry",src:"https://twitter.com/kmcurry/status/1574333302022062080"},{default:a(()=>[e(" needle.tools is a wonderful showcase of what @NeedleTools contributes to 3D via the web. I just love it. ")]),_:1}),s(t,{name:"Stella Cannefax",src:"https://twitter.com/0xstella/status/1574853012585172993"},{default:a(()=>[e(" Thanks to @NeedleTools, seeing quite a bit of this solution for web-based real time 3d tools - export scenes from Unity, where you can leverage the extensive 3d editor ecosystem & content, and then render them in your own web-based engine ")]),_:1}),s(t,{name:"Brit Gardner",src:"https://twitter.com/britg/status/1562443905580163072"},{default:a(()=>[e(" Played with this a bit this morning ๐คฏ๐คฏ pretty magical ")]),_:1}),s(t,{name:"Marc Wakefield",src:"https://twitter.com/mrm_design/status/1567391880169545729"},{default:a(()=>[e(" This is huge for WebXR and shared, immersive 3D experiences! Thank you so much for putting in the work on this @NeedleTools crew! Hoping @Apple sort out their WebXR situation sooner rather than later. The AR part worked flawlessly on my @SamsungMobile S21. ")]),_:1}),s(t,{name:"Pete Patterson",src:"https://twitter.com/VRSpatialist/status/1572300394285383680"},{default:a(()=>[e(" Finally checking out @NeedleTools with Unity. Super easy to get something up and running in the cloud using their glitch integration ")]),_:1}),s(t,{name:"Dilmer Valecillos",src:"https://twitter.com/Dilmerv/status/1562209049856188420"},{default:a(()=>[e(" This is amazing and if you are curious about #WebXR with Unity this will help us get there ")]),_:1}),s(t,{name:"VRSpatialist",src:"https://discord.com/channels/717429793926283276/722046635525537842/1030201907513405530"},{default:a(()=>[e(" I am a long time Unity dev and recently started playing with Needle Tools and I love it! It's a great on ramp for Unity devs who want to learn WebXR and three.js. The runtime engine is awesome and it was pretty easy to create my own custom component ")]),_:1}),s(t,{name:"Unity for Digital Twins",src:"https://twitter.com/DigitalTwin/status/1576934958681055233"},{default:a(()=>[e(" We just gotta say WOW ๐คฉ ")]),_:1}),s(t,{name:"Matthew Pieri",src:"https://discord.com/channels/717429793926283276/1097572505738301571/1097572505738301571"},{default:a(()=>[e(" Spent the last 2.5 months building this game, never built a game/never used unity before, but absolutely loving the whole process with needle tools. So rapid! Would love to make a career building AR experiences! ")]),_:1})])}const f=n(h,[["render",d],["__file","testimonials.html.vue"]]);export{f as default};
diff --git a/assets/testing.html-b56d1138.js b/assets/testing.html-b56d1138.js
new file mode 100644
index 000000000..1aabc8aee
--- /dev/null
+++ b/assets/testing.html-b56d1138.js
@@ -0,0 +1 @@
+import{_ as a,M as r,p as l,q as s,R as e,t,N as n,a1 as i}from"./framework-c782e227.js";const d="/docs/testing/switch-to-mkcert.webp",c={},h=i('
When using our templates, Needle Engine runs a local development server for you. Simply press play, and a website will open in your default browser, ready for testing on your local device. For testing on other devices in the same network, you may have to install a self-signed certificate (see below).
When using a custom setup / framework, please refer to your framework's documentation on how to run a secure local development server.
TIP
Our local server uses the IP address in your local network (e.g. https://192.168.0.123:3000) instead of localhost:3000. This allows you to quickly view and test your project from mobile devices, VR glasses, and other machines in the same network.
We're using HTTPS instead of the older HTTP, because modern powerful web APIs like WebXR require a secure connection โ the S stands for "secure".
# Setting up a self-signed certificate for development
Different operating systems have different security requirements for local development. Typically, displaying a website will work even with a auto-generated untrusted certificate, but browsers may warn about the missing trust and some features are not available. Here's a summary:
TIP
Installing trusted self-signed certificates on your development devices is recommended for a smooth development experience. Find the steps at the bottom of this page.
',7),u=e("strong",null,"Default โ with auto-generated untrusted certificate",-1),p=e("br",null,null,-1),f=e("em",null,"Displays a certificate warning upon opening the project in a browser.",-1),m={href:"https://github.com/vitejs/vite-plugin-basic-ssl",target:"_blank",rel:"noopener noreferrer"},g=i("
We're using websocket connections to communicate between the browser and the local development server. Websockets require a secure connection (HTTPS) in order to work. Depending on the platform, you might need to set up a custom certificate for local development. Android and Windows don't need a custom certificate, but iOS and MacOS do.
OS
Viewing in the browser (with a security warning)
Automatic reloads
Windows
(โ)
โ
Linux
(โ)
โ
Android
(โ)
โ
macOS
(โ)
โ
iOS
(โ Safari and Chrome, after page reload) โ Mozilla XR Viewer
โ
Xcode Simulators
(โ after page reload)
โ
",2),w=e("strong",null,"With a self-signed, trusted root certificate",-1),v=e("br",null,null,-1),y=e("em",null,"No security warning is displayed. You need to install the generated certificate on your device(s).",-1),b=e("br",null,null,-1),k={href:"https://github.com/liuweiGL/vite-plugin-mkcert",target:"_blank",rel:"noopener noreferrer"},_=i('
OS
Viewing in the browser
Automatic reloads
Windows
โ
โ
Linux
โ
โ
Android
โ
โ
macOS
โ
โ
iOS
โ
โ
# Generating a self-signed development certificate
in Unity/Blender, click on "Open Workspace" to open VS Code
check that you're using vite-plugin-mkcert instead of vite-plugin-basic-ssl (the latter doesn't generate a trusted root certificate, which we need) in your vite.config.ts file:
open package.json
remove the line with "@vitejs/plugin-basic-ssl" from devDependencies
open a Terminal in VS Code and run npm install vite-plugin-mkcert --save-dev which will add the latest version
open vite.config.ts and replace import basicSsl from '@vitejs/plugin-basic-ssl' with import mkcert from'vite-plugin-mkcert'
in plugins: [, replace basicSsl(), with mkcert(),
package.json looks like this now:
run npm run start once from VS Code's terminal
on Windows, this will open a new window and guide you through the creation and installation of the certificate
on MacOS, the terminal prompts for your password and then generates and installs the certificate.
if you're getting an error Error: Port 3000 is already in use, please close the server that may still be running from Unity.
the generated certificate will automatically be installed on the machine you generated it on.
you can stop the terminal process again.
from now on, pressing Play in Unity/Blender will use the generated certificate for the local server, and no "security warning" will be shown anymore, since your browser now trusts the local connection.
# Installing the certificate on your development devices
On your development devices, you need to install the generated certificate and allow the OS to trust it. This is different for each OS. For each of them, you'll need the rootCA.pem file that was generated, and send it to the device you want to authenticate.
On Windows: find the certificate in Users/<your-user>/.vite-plugin-mkcert/rootCA.pem On MacOS: find the certificate in Users/<your-user>/.vite-plugin-mkcert/rootCA.pem
Send the device to yourself (e.g. via E-Mail, AirDrop, iCloud, USB, Slack, ...) so that you can access it on your development devices.
Open the file. You'll be prompted to install the certificate.
# Installing the certificate on iOS / iPadOS / VisionOS
Open the file.
You'll be prompted to add the profile to your device. Confirm.
Go to Settings
There will be a new entry "Profile". Select it and allow the profile to be active for this device.
On iOS / iPadOS, you also need to allow "Root Certificate Trust". For this, search for Trust or go to Settings > General > About > Info > Certificate Trust Settings and enable full trust for the root certificate.
TIP
The certificate is automatically installed on the machine you generated it on. For other machines in the local network, follow the steps below to also establish a trusted connection.
# Installing the certificate on another MacOS machine
Open the file. Keychain Access will open and allow you to install the certificate.
You may have to set "Trust" to "Always allow".
# Installing the certificate on another Windows machine
Open certmgr ("Manage user certificates") by typing Windows key + certmgr.
In the left sidebar, select "Trusted Root Certification Authorities".
Right-click on "Certificates" and select "All Tasks > Import".
Select the rootCA.pem file (you may have to change the file type to "all") and follow the instructions.
',16);function S(q,O){const o=r("ExternalLinkIcon");return l(),s("div",null,[h,e("p",null,[u,p,f,e("em",null,[t("Uses the "),e("a",m,[t("vite-plugin-basic-ssl"),n(o)]),t(" npm package.")])]),g,e("p",null,[w,v,y,b,e("em",null,[t("Uses the "),e("a",k,[t("vite-plugin-mkcert"),n(o)]),t(" npm package.")])]),_])}const x=a(c,[["render",S],["__file","testing.html.vue"]]);export{x as default};
diff --git a/assets/testing.html-c7ec7de5.js b/assets/testing.html-c7ec7de5.js
new file mode 100644
index 000000000..d2ce3613c
--- /dev/null
+++ b/assets/testing.html-c7ec7de5.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-53401e42","path":"/testing.html","title":"Testing on local devices","lang":"en-US","frontmatter":{"title":"Testing on local devices","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/testing on local devices.png"}],["meta",{"name":"og:description","content":"---\\nWhen using our templates, Needle Engine runs a local development server for you. Simply press play, and a website will open in your default browser, ready for testing on your local device. For testing on other devices in the same network, you may have to install a self-signed certificate (see below).\\nWhen using a custom setup / framework, please refer to your framework's documentation on how to run a secure local development server.\\n::: tip\\nOur local server uses the IP address in your local network (e.g. https://192.168.0.123:3000) instead of localhost:3000. This allows you to quickly view and test your project from mobile devices, VR glasses, and other machines in the same network.\\nWe're using HTTPS instead of the older HTTP, because modern powerful web APIs like WebXR require a secure connection โ the S stands for 'secure'.\\n:::\\nDifferent operating systems have different security requirements for local development. Typically, displaying a website will work even with a auto-generated untrusted certificate, but browsers may warn about the missing trust and some features are not available. Here's a summary:\\n::: tip\\nInstalling trusted self-signed certificates on your development devices is recommended for a smooth development experience. Find the steps at the bottom of this page.\\n:::\\nDefault โ with auto-generated untrusted certificate\\n_Displays a certificate warning upon opening the project in a browser._\\n_Uses the vite-plugin-basic-ssl npm package._\\nWe're using websocket connections to communicate between the browser and the local development server. Websockets require a secure connection (HTTPS) in order to work. Depending on the platform, you might need to set up a custom certificate for local development. Android and Windows don't need a custom certificate, but iOS an"}]],"description":"---\\nWhen using our templates, Needle Engine runs a local development server for you. Simply press play, and a website will open in your default browser, ready for testing on your local device. For testing on other devices in the same network, you may have to install a self-signed certificate (see below).\\nWhen using a custom setup / framework, please refer to your framework's documentation on how to run a secure local development server.\\n::: tip\\nOur local server uses the IP address in your local network (e.g. https://192.168.0.123:3000) instead of localhost:3000. This allows you to quickly view and test your project from mobile devices, VR glasses, and other machines in the same network.\\nWe're using HTTPS instead of the older HTTP, because modern powerful web APIs like WebXR require a secure connection โ the S stands for 'secure'.\\n:::\\nDifferent operating systems have different security requirements for local development. Typically, displaying a website will work even with a auto-generated untrusted certificate, but browsers may warn about the missing trust and some features are not available. Here's a summary:\\n::: tip\\nInstalling trusted self-signed certificates on your development devices is recommended for a smooth development experience. Find the steps at the bottom of this page.\\n:::\\nDefault โ with auto-generated untrusted certificate\\n_Displays a certificate warning upon opening the project in a browser._\\n_Uses the vite-plugin-basic-ssl npm package._\\nWe're using websocket connections to communicate between the browser and the local development server. Websockets require a secure connection (HTTPS) in order to work. Depending on the platform, you might need to set up a custom certificate for local development. Android and Windows don't need a custom certificate, but iOS an"},"headers":[{"level":2,"title":"Testing on local devices","slug":"testing-on-local-devices","link":"#testing-on-local-devices","children":[]},{"level":2,"title":"Setting up a self-signed certificate for development","slug":"setting-up-a-self-signed-certificate-for-development","link":"#setting-up-a-self-signed-certificate-for-development","children":[{"level":3,"title":"Generating a self-signed development certificate","slug":"generating-a-self-signed-development-certificate","link":"#generating-a-self-signed-development-certificate","children":[]}]},{"level":2,"title":"Installing the certificate on your development devices","slug":"installing-the-certificate-on-your-development-devices","link":"#installing-the-certificate-on-your-development-devices","children":[{"level":3,"title":"Installing the certificate on Android","slug":"installing-the-certificate-on-android","link":"#installing-the-certificate-on-android","children":[]},{"level":3,"title":"Installing the certificate on iOS / iPadOS / VisionOS","slug":"installing-the-certificate-on-ios-ipados-visionos","link":"#installing-the-certificate-on-ios-ipados-visionos","children":[]},{"level":3,"title":"Installing the certificate on another MacOS machine","slug":"installing-the-certificate-on-another-macos-machine","link":"#installing-the-certificate-on-another-macos-machine","children":[]},{"level":3,"title":"Installing the certificate on another Windows machine","slug":"installing-the-certificate-on-another-windows-machine","link":"#installing-the-certificate-on-another-windows-machine","children":[]}]}],"git":{},"filePathRelative":"testing.md"}`);export{e as data};
diff --git a/assets/texture-compression-8cd31165.js b/assets/texture-compression-8cd31165.js
new file mode 100644
index 000000000..e374e6f24
--- /dev/null
+++ b/assets/texture-compression-8cd31165.js
@@ -0,0 +1 @@
+const e="/docs/blender/texture-compression.webp";export{e as _};
diff --git a/assets/typescript-decorators.html-3572e40d.js b/assets/typescript-decorators.html-3572e40d.js
new file mode 100644
index 000000000..724595c82
--- /dev/null
+++ b/assets/typescript-decorators.html-3572e40d.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-6d28ca94","path":"/reference/typescript-decorators.html","title":"Typescript Decorators","lang":"en-US","frontmatter":{"title":"Typescript Decorators","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/typescript decorators.png"}],["meta",{"name":"og:description","content":"---\\nThe following table contains available Typescript decorators that Needle Engine provides.\\nYou can think of them as Attributes on steroids (if you are familiar with C#)"}]],"description":"---\\nThe following table contains available Typescript decorators that Needle Engine provides.\\nYou can think of them as Attributes on steroids (if you are familiar with C#)"},"headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[{"level":3,"title":"Serializable","slug":"serializable","link":"#serializable","children":[]},{"level":3,"title":"SyncField","slug":"syncfield","link":"#syncfield","children":[]},{"level":3,"title":"Validate","slug":"validate","link":"#validate","children":[]},{"level":3,"title":"Prefix","slug":"prefix","link":"#prefix","children":[]}]}],"git":{},"filePathRelative":"reference/typescript-decorators.md"}');export{e as data};
diff --git a/assets/typescript-decorators.html-d6b4ae86.js b/assets/typescript-decorators.html-d6b4ae86.js
new file mode 100644
index 000000000..c2e59c4e9
--- /dev/null
+++ b/assets/typescript-decorators.html-d6b4ae86.js
@@ -0,0 +1,51 @@
+import{_ as e,M as t,p as o,q as p,R as n,t as c,N as i,a1 as s}from"./framework-c782e227.js";const l={},r=s(`
The following table contains available Typescript decorators that Needle Engine provides.
You can think of them as Attributes on steroids (if you are familiar with C#) - they can be added to classes, fields or methods in Typescript to provide additional functionality.
Field & Property Decorators
@serializable()
Add to exposed / serialized fields. Is used when loading glTF files that have been exported with components from Unity or Blender.
@syncField()
Add to a field to network the value when it changes. You can pass in a method to be called when the field changes
@validate()
Add to receive callbacks in the component event method onValidate whenever the value changes. This behaves similar to Unity's onValidate.
Method Decorators
@prefix(<type>) (experimental)
Can be used to easily inject custom code into other components. Optionally return false to prevent the original method from being executed. See the example below
Class Decorators
@registerType
No argument. Can be added to a custom component class to be registered to the Needle Engine types and to enable hot reloading support.
exportclassButtonObjectextendsBehaviour{
+ // you can omit the type if it's a primitive
+ // e.g. Number, String or Bool
+ @serializable()
+ myNumber:number=42;
+
+ // otherwise add the concrete type that you want to serialize to
+ @serializable(EventList)
+ onClick?: EventList;
+
+ @serializable(SomeComponentType)
+ myComponent: SomeComponentType;
+
+ // Note that for arrays you still add the concrete type (not the array)
+ @serializable(Object3D)
+ myObjects: Object3D[];
+}
+
import{ Camera }from"@needle-tools/engine";
+classYourClass{
+ @prefix(Camera)// < this is type that has the method you want to change
+ awake(){// < this is the method name you want to change
+
+ // this is now called before the Camera.awake method runs
+ // NOTE: \`this\` does now refer to the Camera instance and NOT \`YourClass\` anymore. This allows you to access internal state of the component as well
+ console.log("Hello camera:",this)
+ // optionally return false if you want to prevent the default behaviour
+ }
+}
+
`,1);function k(m,v){const a=t("ExternalLinkIcon");return o(),p("div",null,[r,n("p",null,[n("a",u,[c("Live example"),i(a)])]),d])}const h=e(l,[["render",k],["__file","typescript-decorators.html.vue"]]);export{h as default};
diff --git a/assets/typescript-essentials.html-6ea8109d.js b/assets/typescript-essentials.html-6ea8109d.js
new file mode 100644
index 000000000..e777715d9
--- /dev/null
+++ b/assets/typescript-essentials.html-6ea8109d.js
@@ -0,0 +1,83 @@
+import{_ as o,M as p,p as c,q as i,R as n,t as s,N as e,a1 as t}from"./framework-c782e227.js";const l={},r=n("p",null,"The following guide tries to highlight some of the key differences between C#, Javascript and Typescript. This is most useful for developers new to the web ecosystem.",-1),u=n("p",null,"Here are also some useful resources for learning how to write Typescript:",-1),d={href:"https://www.typescripttutorial.net/",target:"_blank",rel:"noopener noreferrer"},k={href:"https://www.tutorialsteacher.com/typescript",target:"_blank",rel:"noopener noreferrer"},m={href:"https://www.typescriptlang.org/docs/",target:"_blank",rel:"noopener noreferrer"},h=t('
# Key differences between C#, Javascript or Typescript
CSharp or C# is a statically typed & compiled language. It means that before your code can run (or be executed) it has to be compiled - translated - into IL or CIL, an intermediate language that is a little closer to machine code. The important bit to understand here is that your code is analyzed and has to pass certain checks and rules that are enforced by the compiler. You will get compiler errors in Unity and your application not even start running if you write code that violates any of the rules of the C# language. You will not be able to enter Play-Mode with compiler errors.
Javascript on the other hand is interpreted at runtime. That means you can write code that is not valid and cause errors - but you will not see those errors until your program runs or tries to execute exactly that line that has the error. For example you can write var points = 100; points += "hello world"; and nobody will complain until you run the code in a browser.
Typescript is a language designed by Microsoft that compiles to javascript It adds a lot of features like for example type-safety. That means when you write code in Typescript you can declare types and hence get errors at compile-time when you try to e.g. make invalid assignments or call methods with unexpected types. Read more about types in Javascript and Typescript below.
Vanilla Javascript does (as of today) not have any concept of types: there is no guarantuee that a variable that you declared as let points = 100 will still be a number later in your application. That means that in Javascript it is perfectly valid code to assign points = new Vector3(100, 0, 0); later in your code. Or even points = null or points = myRandomObject - you get the idea. This is all OK while you write the code but it may crash horrible when your code is executed because later you write points -= 1 and now you get errors in the browser when your application is already running.
As mentioned above Typescript was created to help fix that problem by adding syntax for defining types.
It is important to understand that you basically still write Javascript when you write Typescript and while it is possible to circumvent all type checking and safety checks by e.g. adding //@ts-ignore above a erroneous line or defining all types as any this is definitely not recommneded. Types are here to help you find errors before they actually happen. You really dont want to deploy your website to your server only to later get reports from users or visitors telling you your app crashed while it was running.
In C# you write variables either by using the type or the var keyword. For example you can either write int points = 100; or alternatively use var and let the compiler figure out the correct type for you: var points = 100
In Javascript or Typescript you have two modern options to declaring a variable. For a variable that you plan to re-assign use let, for example let points = 100; For a variable that you do not want to be able to re-assign use const, for example const points = 100;
',3),g=n("em",null,"Be aware of var",-1),w=n("br",null,null,-1),f=n("code",null,"var",-1),_=n("code",null,"let",-1),x={href:"https://stackoverflow.com/a/11444416",target:"_blank",rel:"noopener noreferrer"},E=t(`
Please note that you can still assign values to variables declared with const if they are (for example) a custom type. Consider the following example:
const myPosition : Vector3 =newVector3(0,0,0);
+myPosition.x =100;// Assigning x is perfectly fine
+
The above is perfectly fine Typescript code because you don't re-assign myPosition but only the x member of myPosition. On the other hand the following example would not be allowed and cause a runtime or typescript error:
const myPosition : Vector3 =newVector3(0,0,0);
+myPosition =newVector3(100,0,0);// โ ASSIGNING TO CONST IS NOT ALLOWED
+
In Unity you usually add using statements at the top of you code to import specific namespaces from Assemblies that are references in your project or - in certain cases - you migth find yourself importing a specific type with a name from a namespace. See the following example:
usingUnityEngine;
+// importing just a specific type and giving it a name
+usingMonoBehaviour=UnityEngine.MonoBehaviour;
+
This is how you do the same in Typescript to import specific types from a package:
Vector2, Vector3, Vector4... If you have a C# background you might be familiar with the difference between a class and a struct. While a class is a reference type a struct is a custom value type. Meaning it is, depending on the context, allocated on the stack and when being passed to a method by default a copy is created. Consider the following example in C#:
voidMyCallerMethod(){
+ var position =newVector3(0,0,0);
+ MyExampleVectorMethod(position);
+ UnityEngine.Debug.Log("Position.x is "+ position.x);// Here x will be 0
+}
+voidMyExampleVectorMethod(Vector3 position){
+ position.x =42;
+}
+
A method is called with a Vector3 named position. Inside the method the passed in vector position is modified: x is set to 42. But in C# the original vector that is being passed into this method (see line 2) is not changed and x will still be 0 (line 4).
The same is not true for Javascript/Typescript. Here we don't have custom value types, meaning if you come across a Vector in Needle Engine or three.js you will always have a reference type. Consider the following example in typescript:
import{ Vector3 }from"three"
+functionmyCallerMethod():void{
+ const position =newVector(0,0,0);
+ myExampleVectorMethod(position);
+ console.log("Position.x is "+ position.x);// Here x will be 42
+}
+functionmyExampleVectorMethod(position: Vector3):void{
+ position.x =42;
+}
+
Do you see the difference? Because vectors and all custom objects are in fact reference types we will have modified the original position variable (line 3) and x is now 42.
This is not only important to understand for methods but also when working with variables. In C# the following code will produce two instances of Vector3 and changing one will not affect the other:
var myVector =newVector3(1,1,1);
+var myOtherVector = myVector;
+myOtherVector.x =42;
+// will log: 1, 42
+UnityEngine.Debug.Log(myVector.x +", "+ myOtherVector.x);
+
If you do the same in Typescript you will not create a copy but get a reference to the same myVector instance instead:
While in C# you can use operator overloading this is not available in Javascript unfortunately. This means that while you can multiply a Vector3 in C# like this:
var myFirstVector =newVector3(1,1,1);
+var myFactor =100f;
+myFirstVector *= myFactor;
+// โ myFirstVector is now 100, 100, 100
+
you have to use a method on the Vector3 type to archieve the same result (just with a little more boilerplate code)
const myFirstVector : Vector3 =newVector3(1,1,1)
+const myFactor = 100f;
+myFirstVector.multiplyScalar(myFactor);
+// โ myFirstVector is now 100, 100, 100
+
When you subscribe to an Event in C# you do it like this:
// this is how an event is declared
+eventAction MyEvent;
+// you subscribe by adding to (or removing from)
+voidOnEnable(){
+ MyEvent += OnMyEvent;
+}
+voidOnDisable(){
+ MyEvent -= OnMyEvent;
+}
+voidOnMyEvent(){}
+
In Typescript and Javascript when you add a method to a list you have to "bind this". That essentially means you create a method where you explictly set this to (usually) your current class instance. There are two way to archieve this.
Please note that we are using the type EventList here which is a Needle Engine type to declare events (the EventList will also automatically be converted to a UnityEvent and or a event list in Blender when you use them with our Editor integrations)
myEvent?: EventList;
+voidonEnable(){
+ this.myEvent.addEventListener(this.onMyEvent);
+}
+voidonDisable(){
+ this.myEvent.removeEventListener(this.onMyEvent);
+}
+// Declaring the function as an arrow method
+// to automatically bind this:
+privateonMyEvent=()=>{
+ console.log(this!==undefined,this)
+ }
+
There is also the more verbose "classical" way to archieve the same thing by manually binding this (and saving the method in a variable to later remove it again from the event list):
myEvent?: EventList;
+private _onMyEventFn?:Function;
+voidonEnable(){
+ // bind this
+ this._onMyEventFn =this.onMyEvent.bind(this);
+ // add the bound method to the event
+ this.myEvent?.addEventListener(this._onMyEventFn);
+}
+voidonDisable(){
+ this.myEvent?.removeEventListener(this._onMyEventFn);
+}
+
`,3);function N(P,S){const a=p("ExternalLinkIcon");return c(),i("div",null,[r,u,n("ul",null,[n("li",null,[n("a",d,[s("Typescript Tutorial"),e(a)])]),n("li",null,[n("a",k,[s("Learn Typescript"),e(a)])]),n("li",null,[n("a",m,[s("Typescript Documentation"),e(a)])])]),h,n("p",null,[s("While "),v,s(" does not offer types you can still add type-annotations to your javascript variables, classes and methods by using "),n("strong",null,[n("a",y,[s("JSDoc"),e(a)])]),s(".")]),b,n("blockquote",null,[n("p",null,[g,w,s(" You might come across the "),f,s(" keyword in javascript as well but it is not recommended to use it and the modern replacement for it is "),_,s(". Learn more about "),n("a",x,[s("var vs let"),e(a)]),s(".")])]),E,n("p",null,[s("You notice that the second variable "),V,s(" is using "),T,s(" which does a loose equality check in which case "),M,s(" and "),I,s(" will both result in "),C,s("here. You can read more about that "),n("a",O,[s("here"),e(a)])]),q,n("p",null,[s("The short and "),F,s(" syntax for doing this is to use "),n("a",L,[s("Arrow Functions"),e(a)]),s(".")]),J])}const U=o(l,[["render",N],["__file","typescript-essentials.html.vue"]]);export{U as default};
diff --git a/assets/typescript-essentials.html-70936264.js b/assets/typescript-essentials.html-70936264.js
new file mode 100644
index 000000000..db70926e3
--- /dev/null
+++ b/assets/typescript-essentials.html-70936264.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-2e0e70fc","path":"/getting-started/typescript-essentials.html","title":"Typescript Essentials","lang":"en-US","frontmatter":{"title":"Typescript Essentials","sidebarDepth":2,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/typescript essentials.png"}],["meta",{"name":"og:description","content":"---\\nThe following guide tries to highlight some of the key differences between C#, Javascript and Typescript. This is most useful for developers new to the web ecosystem.\\nHere are also some useful resources for learning how to write Typescript:"}]],"description":"---\\nThe following guide tries to highlight some of the key differences between C#, Javascript and Typescript. This is most useful for developers new to the web ecosystem.\\nHere are also some useful resources for learning how to write Typescript:"},"headers":[{"level":3,"title":"Key differences between C#, Javascript or Typescript","slug":"key-differences-between-c-javascript-or-typescript","link":"#key-differences-between-c-javascript-or-typescript","children":[]},{"level":3,"title":"Types โ or the lack thereof","slug":"types-or-the-lack-thereof","link":"#types-or-the-lack-thereof","children":[]},{"level":3,"title":"Variables","slug":"variables","link":"#variables","children":[]},{"level":3,"title":"Using or Importing Types","slug":"using-or-importing-types","link":"#using-or-importing-types","children":[]},{"level":3,"title":"Primitive Types","slug":"primitive-types","link":"#primitive-types","children":[]},{"level":3,"title":"Vector Maths and Operators","slug":"vector-maths-and-operators","link":"#vector-maths-and-operators","children":[]},{"level":3,"title":"Equality Checks","slug":"equality-checks","link":"#equality-checks","children":[]},{"level":3,"title":"Events, Binding and this","slug":"events-binding-and-this","link":"#events-binding-and-this","children":[]}],"git":{},"filePathRelative":"getting-started/typescript-essentials.md"}');export{e as data};
diff --git a/assets/vanilla-js.html-28b32fef.js b/assets/vanilla-js.html-28b32fef.js
new file mode 100644
index 000000000..8845cc651
--- /dev/null
+++ b/assets/vanilla-js.html-28b32fef.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-7acec8c5","path":"/vanilla-js.html","title":"Vanilla Javascript","lang":"en-US","frontmatter":{"title":"Vanilla Javascript","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/vanilla javascript.png"}],["meta",{"name":"og:description","content":"---\\nThe following is an example for how to use Needle Engine with vanilla javascript / without using a bundler.\\nSee it live here.\\nInstall vscode live-server extension.\\nThen open this index.html with the live-server and navigate to http://localhost:5500/index.html in your web browser.\\n::: tip\\nMake sure to update the path to an existing glb file\\nor download this sample glb and put it in the same folder as the index.html, name it myScene.glb or update the src path.\\n:::\\n@code\\nView on github"}]],"description":"---\\nThe following is an example for how to use Needle Engine with vanilla javascript / without using a bundler.\\nSee it live here.\\nInstall vscode live-server extension.\\nThen open this index.html with the live-server and navigate to http://localhost:5500/index.html in your web browser.\\n::: tip\\nMake sure to update the path to an existing glb file\\nor download this sample glb and put it in the same folder as the index.html, name it myScene.glb or update the src path.\\n:::\\n@code\\nView on github"},"headers":[{"level":3,"title":"How to run locally","slug":"how-to-run-locally","link":"#how-to-run-locally","children":[]}],"git":{},"filePathRelative":"vanilla-js.md"}');export{e as data};
diff --git a/assets/vanilla-js.html-615fb392.js b/assets/vanilla-js.html-615fb392.js
new file mode 100644
index 000000000..fa73bd5f7
--- /dev/null
+++ b/assets/vanilla-js.html-615fb392.js
@@ -0,0 +1,53 @@
+import{_ as e,M as p,p as o,q as l,R as n,t as s,N as t,a1 as c}from"./framework-c782e227.js";const i={},u=n("h1",{id:"basic-html",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#basic-html","aria-hidden":"true"},"#"),s(" Basic html")],-1),r=n("br",null,null,-1),k={href:"https://engine.needle.tools/samples/vanillajs/",target:"_blank",rel:"noopener noreferrer"},d=n("h3",{id:"how-to-run-locally",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#how-to-run-locally","aria-hidden":"true"},"#"),s(" How to run locally")],-1),v={href:"https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer",target:"_blank",rel:"noopener noreferrer"},g=n("br",null,null,-1),m=n("code",null,"http://localhost:5500/index.html",-1),h={class:"custom-container tip"},b=n("p",{class:"custom-container-title"},"TIP",-1),q=n("code",null,'',-1),_=n("br",null,null,-1),y={href:"https://github.com/needle-tools/needle-engine-samples/raw/main/vanilla/myScene.glb",target:"_blank",rel:"noopener noreferrer"},f=n("code",null,"myScene.glb",-1),w=c(`
<!DOCTYPEhtml>
+<htmllang="en">
+
+<head>
+ <metacharset="UTF-8"/>
+ <linkrel="icon"href="favicon.ico">
+ <metaname="viewport"content="width=device-width, user-scalable=no">
+ <title>Made with Needle</title>
+
+ <!-- importmap -->
+ <scripttype="importmap">
+ {
+ "imports":{
+ "three":"https://unpkg.com/three/build/three.module.js",
+ "three/":"https://unpkg.com/three/"
+ }
+ }
+ </script>
+ <!-- parcel require must currently defined somewhere for peerjs -->
+ <script>var parcelRequire;</script>
+
+ <!-- the .light version does not include dependencies like Rapier.js (so no physics) to reduce the bundle size, if your project needs physics then just change it to needle-engine.min.js -->
+ <scripttype="module"src="https://unpkg.com/@needle-tools/engine@3.5.6-alpha/dist/needle-engine.light.min.js"></script>
+
+ <style>
+ body{margin: 0;}
+ needle-engine{
+ position: absolute;
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: grey;
+ }
+ </style>
+
+</head>
+
+<body>
+
+ <!-- load a gltf or glb here as your scene and listen to the finished event to start interacting with it -->
+ <needle-enginesrc="myScene.glb"loadfinished="onLoaded"></needle-engine>
+
+</body>
+
+<script>
+ functiononLoaded(ctx){
+ console.log("Loading a glb file finished ๐");
+ console.log("This is the scene: ", ctx.scene);
+ }
+</script>
+
+</html>
+
`,1),x={href:"https://github.com/needle-tools/needle-engine-samples/tree/main/vanilla",target:"_blank",rel:"noopener noreferrer"};function j(N,S){const a=p("ExternalLinkIcon");return o(),l("div",null,[u,n("p",null,[s("The following is an example for how to use Needle Engine with vanilla javascript / without using a bundler."),r,s(" See it "),n("a",k,[s("live here"),t(a)]),s(".")]),d,n("p",null,[s("Install "),n("a",v,[s("vscode live-server extension"),t(a)]),s("."),g,s(" Then open this index.html with the live-server and navigate to "),m,s(" in your web browser.")]),n("div",h,[b,n("p",null,[s("Make sure to update the "),q,s(" path to an existing glb file"),_,s(" or "),n("a",y,[s("download this sample glb"),t(a)]),s(" and put it in the same folder as the index.html, name it "),f,s(" or update the src path.")])]),w,n("p",null,[n("a",x,[s("View on github"),t(a)])])])}const L=e(i,[["render",j],["__file","vanilla-js.html.vue"]]);export{L as default};
diff --git a/assets/video-embed-f1b6f8f5.js b/assets/video-embed-f1b6f8f5.js
new file mode 100644
index 000000000..cbd0827b4
--- /dev/null
+++ b/assets/video-embed-f1b6f8f5.js
@@ -0,0 +1 @@
+import{_ as c,p as s,q as i,R as a,Q as l,ad as _}from"./framework-c782e227.js";const d={props:{src:String,controls:Boolean,limit_height:Boolean,max_height:String}},o=d,n=()=>{_(e=>({"5159d46b":e.limit_height?"400px":"initial","4cc05253":e.limit_height?e.max_height:"initial","49dcf46c":e.limit_height?"auto":"100%","25a42c34":e.limit_height?"100%":"auto","02f9f579":e.limit_height?e.max_height:"100%"}))},r=o.setup;o.setup=r?(e,t)=>(n(),r(e,t)):n;const p={key:0,class:"container"},h=["src"],m={key:1,class:"container"},u=["src"];function f(e,t,v,g,y,b){return e.src.includes("youtube.com")?(s(),i("div",p,[a("iframe",{id:"ytplayer",class:"video",src:e.src.replace("watch?v=","embed/")+"?autoplay=0&origin=http://docs.needle.tools&controls=1&loop=1&modestbranding=1&showinfo=0&color=white",frameborder:"0",allowfullscreen:""},null,8,h)])):(s(),i("div",m,[l(' '),a("video",{loop:"",autoplay:"",controls:"",src:e.src},null,8,u)]))}const k=c(o,[["render",f],["__scopeId","data-v-144c2fbe"],["__file","video-embed.vue"]]);export{k as default};
diff --git a/assets/vision.html-9288f957.js b/assets/vision.html-9288f957.js
new file mode 100644
index 000000000..ee9ccfa19
--- /dev/null
+++ b/assets/vision.html-9288f957.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-2ace8550","path":"/vision.html","title":"Our Vision ๐ฎ","lang":"en-US","frontmatter":{"next":"features-overview","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/vision.png"}],["meta",{"name":"og:description","content":"---\\nWe believe the use of 3D on the web will expand considerably in the next years. While today native apps are the norm, more and more content is made available as a web app or PWA. New VR and AR devices will extend into the web, creating an interesting problem: responsive suddenly not only means 'small screen' or 'large screen', you're also dealing with spaces, 3D, spatial placement and potentially glasses and controllers!\\nAdd to that a push towards more interactivity and collaboration, and you have an interesting mix of challenges.\\nAt Needle, we believe ideating and creating in this space should be easy. We've set out to speed things up โ creating our own runtime to reach these goals. That's why we're baking the ability to deploy to AR and VR right into our core components, and continually test that new ideas work across platforms.\\nThere's numerous options, that's true! We found that current systems1 can be roughly sorted into two categories: some have great asset handling, tools, and artist-friendly workflows but output some sort of binary blob, and others are more code-focussed, developer-friendly and allow for great integration into modern web workflows2.\\nWe want to bridge these worlds and combine the best of both worlds: artist-friendly workflows and modern web technologies. Combined with modern formats and a snappy workflow, we believe this will allow many more creators to bring their content to the web. We also saw an opportunity to get AR, VR and collaboration right from the start.\\n1: _Examples include Unity, PlayCanvas, three.js, react-three-fiber, Babylon, A-Frame, Godot, and many more._\\n2: _There's more nuance to this than fits into an introductory p"}]],"description":"---\\nWe believe the use of 3D on the web will expand considerably in the next years. While today native apps are the norm, more and more content is made available as a web app or PWA. New VR and AR devices will extend into the web, creating an interesting problem: responsive suddenly not only means 'small screen' or 'large screen', you're also dealing with spaces, 3D, spatial placement and potentially glasses and controllers!\\nAdd to that a push towards more interactivity and collaboration, and you have an interesting mix of challenges.\\nAt Needle, we believe ideating and creating in this space should be easy. We've set out to speed things up โ creating our own runtime to reach these goals. That's why we're baking the ability to deploy to AR and VR right into our core components, and continually test that new ideas work across platforms.\\nThere's numerous options, that's true! We found that current systems1 can be roughly sorted into two categories: some have great asset handling, tools, and artist-friendly workflows but output some sort of binary blob, and others are more code-focussed, developer-friendly and allow for great integration into modern web workflows2.\\nWe want to bridge these worlds and combine the best of both worlds: artist-friendly workflows and modern web technologies. Combined with modern formats and a snappy workflow, we believe this will allow many more creators to bring their content to the web. We also saw an opportunity to get AR, VR and collaboration right from the start.\\n1: _Examples include Unity, PlayCanvas, three.js, react-three-fiber, Babylon, A-Frame, Godot, and many more._\\n2: _There's more nuance to this than fits into an introductory p"},"headers":[{"level":2,"title":"The Future of the 3D Web","slug":"the-future-of-the-3d-web","link":"#the-future-of-the-3d-web","children":[]},{"level":2,"title":"Why another platform for 3D on the web? Aren't there enough options already?","slug":"why-another-platform-for-3d-on-the-web-aren-t-there-enough-options-already","link":"#why-another-platform-for-3d-on-the-web-aren-t-there-enough-options-already","children":[]},{"level":2,"title":"Creating a Workflow, not an Editor","slug":"creating-a-workflow-not-an-editor","link":"#creating-a-workflow-not-an-editor","children":[]},{"level":2,"title":"Open Standards instead of Proprietary Containers","slug":"open-standards-instead-of-proprietary-containers","link":"#open-standards-instead-of-proprietary-containers","children":[]},{"level":2,"title":"Goals","slug":"goals","link":"#goals","children":[]},{"level":2,"title":"Non-Goals","slug":"non-goals","link":"#non-goals","children":[]},{"level":2,"title":"Needle Engine and Unity WebGL","slug":"needle-engine-and-unity-webgl","link":"#needle-engine-and-unity-webgl","children":[]},{"level":2,"title":"Needle Engine and three.js","slug":"needle-engine-and-three.js","link":"#needle-engine-and-three.js","children":[]}],"git":{},"filePathRelative":"vision.md"}`);export{e as data};
diff --git a/assets/vision.html-9d1df3bf.js b/assets/vision.html-9d1df3bf.js
new file mode 100644
index 000000000..70aac2f6a
--- /dev/null
+++ b/assets/vision.html-9d1df3bf.js
@@ -0,0 +1 @@
+import{_ as s,M as n,p as l,q as d,R as t,t as e,N as o,V as h,a1 as r}from"./framework-c782e227.js";const c={},p=t("h1",{id:"our-vision",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#our-vision","aria-hidden":"true"},"#"),e(" Our Vision ๐ฎ")],-1),u=t("h2",{id:"the-future-of-the-3d-web",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#the-future-of-the-3d-web","aria-hidden":"true"},"#"),e(" The Future of the 3D Web")],-1),g={href:"https://web.dev/progressive-web-apps/",target:"_blank",rel:"noopener noreferrer"},b={href:"https://immersive-web.github.io/webxr-samples/",target:"_blank",rel:"noopener noreferrer"},m=r('
Add to that a push towards more interactivity and collaboration, and you have an interesting mix of challenges.
At Needle, we believe ideating and creating in this space should be easy. We've set out to speed things up โ creating our own runtime to reach these goals. That's why we're baking the ability to deploy to AR and VR right into our core components, and continually test that new ideas work across platforms.
# Why another platform for 3D on the web? Aren't there enough options already?
There's numerous options, that's true! We found that current systems1 can be roughly sorted into two categories: some have great asset handling, tools, and artist-friendly workflows but output some sort of binary blob, and others are more code-focussed, developer-friendly and allow for great integration into modern web workflows2.
We want to bridge these worlds and combine the best of both worlds: artist-friendly workflows and modern web technologies. Combined with modern formats and a snappy workflow, we believe this will allow many more creators to bring their content to the web. We also saw an opportunity to get AR, VR and collaboration right from the start.
1: Examples include Unity, PlayCanvas, three.js, react-three-fiber, Babylon, A-Frame, Godot, and many more.2: There's more nuance to this than fits into an introductory paragraph! All engines and frameworks have their strengths and weaknesses, and are constantly evolving.
We think the next wave of 3D apps on the web will come with better workflows: everyone should be able to put together a 3D scene, an art gallery, present a product or 3D scan on the web or make simple games. Reaching this goal will require more than just supporting one particular system and exporting to the web from there.
Our goal is to allow people to bring data to the web from their creative tools: be it Unity, Blender, Photoshop, or something else. We're aware that this is a big goal โ but instead of doing everything at once, we want to iterate and get closer to it together.
# Open Standards instead of Proprietary Containers
',10),w={href:"https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html",target:"_blank",rel:"noopener noreferrer"},f=t("code",null,".glb",-1),y=t("p",null,"It's worth noting that it's not a goal to ship actual code inside glTF; shipping and running code is the job of modern web runtimes and bundling. We certainly can imagine that abstract representations of logic (e.g. graphs, state machines, and so on) can be standardized to a certain degree and allow for interoperable worlds, but we're not there yet.",-1),v=r('
From working with Unity for many years we've found that while the engine and editor progress at a great pace, WebGL output has somewhat lacked behind. Integration of Unity players into web-based systems is rather hard, "talking" to the surrounding website requires a number of workarounds, and most of all, iteration times are very slow due to the way that Unity packs all code into WebAssembly via IL2CPP. These technologies are awesome, and result in great runtime performance and a lot of flexibility. But they're so much slower and walled off compared to modern web development workflows that we decided to take matters into our own hands.
Needle Engine builds on three.js. All rendering goes through it, glTF files are loaded via three's extension interfaces, and our component system revolves around three's Object3D and scene graph. We're committed to upstreaming some of our changes and improvements, creating pull requests and reporting issues along the way.
',10);function k(_,x){const a=n("ExternalLinkIcon"),i=n("RouterLink");return l(),d("div",null,[p,u,t("p",null,[e("We believe the use of 3D on the web will expand considerably in the next years. While today native apps are the norm, more and more content is made available as a web app or "),t("a",g,[e("PWA"),o(a)]),e(". New VR and AR devices will "),t("a",b,[e("extend into the web"),o(a)]),e(`, creating an interesting problem: responsive suddenly not only means "small screen" or "large screen", you're also dealing with spaces, 3D, spatial placement and potentially glasses and controllers!`)]),m,t("p",null,[e("At the core of Needle Engine stands the "),t("a",w,[e("glTF"),o(a)]),e(" format and its ability to be extended with custom extensions. The goal is: a single "),f,e(" file can contain your entire application's data.")]),y,t("p",null,[o(i,{to:"/technical-overview.html"},{default:h(()=>[e("Read more about our use of glTF and extensions")]),_:1})]),v])}const A=s(c,[["render",k],["__file","vision.html.vue"]]);export{A as default};
diff --git a/assets/xr.html-aa491a88.js b/assets/xr.html-aa491a88.js
new file mode 100644
index 000000000..667cfd3c8
--- /dev/null
+++ b/assets/xr.html-aa491a88.js
@@ -0,0 +1,16 @@
+import{_ as d,M as i,p,q as u,R as e,t as n,N as t,V as s,a1 as r}from"./framework-c782e227.js";const h={},m=r('
Theoretically all WebXR-capable devices and browsers should be supported. That being said, we've tested the following configurations:
Tested VR Device
Browser
Notes
Meta Quest 1
โ๏ธ Meta Browser
hand tracking, support for sessiongranted1
Meta Quest 2
โ๏ธ Meta Browser
hand tracking, support for sessiongranted1, passthrough (black and white)
Meta Quest Pro
โ๏ธ Meta Browser
hand tracking, support for sessiongranted1, passthrough
Pico Neo 3
โ๏ธ Pico Browser
no hand tracking, inverted controller thumbsticks
Pico Neo 4
โ๏ธ Pico Browser
passthrough, hand tracking2
Oculus Rift 1/2
โ๏ธ Chrome
Hololens 2
โ๏ธ Edge
hand tracking, support for AR and VR (in VR mode, background is rendered as well)
Meta Quest 1/2 via Oculus Link
โ๏ธ Chrome
no hand tracking, known issues with repeated refresh of a WebXR session in Chrome
Looking Glass Portrait
โ๏ธ Chrome
requires shim, see samples
',3),g=e("thead",null,[e("tr",null,[e("th",null,"Tested AR Device"),e("th",null,"Browser"),e("th",null,"Notes")])],-1),b=e("tr",null,[e("td",null,"Android 10+"),e("td",null,"โ๏ธ Chrome"),e("td")],-1),_=e("tr",null,[e("td",null,"Android 10+"),e("td",null,"โ๏ธ Firefox"),e("td")],-1),f=e("tr",null,[e("td",null,"iOS 15+"),e("td",null,"โ๏ธ WebXR Viewer"),e("td",null,"does not fully implement standards, but supported")],-1),v=e("td",null,"iOS 15+",-1),k=e("td",null,[n("(โ๏ธ)"),e("sup",null,"3"),n(" Safari")],-1),y=e("tr",null,[e("td",null,"Hololens 2"),e("td",null,"โ๏ธ Edge"),e("td")],-1),w=e("tr",null,[e("td",null,"Hololens 1"),e("td",null,"โ"),e("td",null,"no WebXR support")],-1),R=e("tr",null,[e("td",null,"Magic Leap 2"),e("td",null,"โ๏ธ"),e("td")],-1),A=e("table",null,[e("thead",null,[e("tr",null,[e("th",null,"Not Tested but Should Workโข๏ธ"),e("th",null,"Browser"),e("th",null,"Notes")])]),e("tbody",null,[e("tr",null,[e("td",null,"Magic Leap 1"),e("td"),e("td",null,"please let us know if you tried!")])])],-1),x=e("sup",null,"1",-1),S=e("code",null,"chrome://flags/#webxr-navigation-permission",-1),T=e("br",null,null,-1),E=e("sup",null,"2",-1),j=e("br",null,null,-1),O=e("sup",null,"3",-1),q=e("a",{href:"#augmented-reality-and-webxr-on-ios"},"other approaches",-1),W=e("h2",{id:"adding-vr-and-ar-capabilities-to-a-scene",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#adding-vr-and-ar-capabilities-to-a-scene","aria-hidden":"true"},"#"),n(" Adding VR and AR capabilities to a scene")],-1),C=e("p",null,"AR, VR and networking capabilites in Needle Engine are designed to be modular. You can choose to not support any of them, or add only specific features.",-1),X=e("h3",{id:"basic-capabilities",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#basic-capabilities","aria-hidden":"true"},"#"),n(" Basic capabilities")],-1),B=r('
Enable AR and VR Add a WebXR component. Optional: you can set a custom avatar by referencing an Avatar Prefab. By default a very basic DefaultAvatar is assigned.
Enable Teleportation Add a TeleportTarget component to object hierarchies that can be teleported on. To exclude specific objects, set their layer to IgnoreRaycasting.
',2),V=e("strong",null,"Enable going into Miniature Mode",-1),M=e("br",null,null,-1),N=e("br",null,null,-1),P=r('
Define the AR Session Root and Scale Add a WebARSessionRoot component to your root object. Here you can define the user scale to shrink (< 1) or enlarge (> 1) the user in relation to the scene when entering AR.
Define whether an object is visible in Browser, AR, VR, First Person, Third Person Add a XR Flag component to the object you want to control. Change options on the dropdown as needed.
Common usecases are
hiding floors when entering AR
hiding Avatar parts in First or Third Person views (e.g. first-person head shouldn't be visible).
',5),G={href:"https://github.com/immersive-web/navigation",target:"_blank",rel:"noopener noreferrer"},H=e("code",null,"sessiongranted",-1),F=e("p",null,[n("Currently, this is only supported on Oculus Quest 1 and 2 in the Oculus Browser. On other platforms, users will be kicked out of their current immersive session and have to enter VR again on the new page."),e("br"),n(" Requires enabling a browser flag: "),e("code",null,"chrome://flags/#webxr-navigation-permission")],-1),U=e("ul",null,[e("li",null,[e("strong",null,"Click on objects to open links"),e("br"),n(" Add the "),e("code",null,"OpenURL"),n(" component that makes it very easy to build connected worlds.")])],-1),Q=e("h2",{id:"scripting",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#scripting","aria-hidden":"true"},"#"),n(" Scripting")],-1),z=r(`
There's a number of experimental components to build more expressive Avatars. At this point we recommended duplicating them to make your own variants, since they might be changed or removed at a later point.
Example Avatar Rig with basic neck model and limb constraints
Random Player Colors As an example for avatar customization, you can add a PlayerColor component to your renderers. This randomized color is synchronized between players.
Eye Rotation AvatarEyeLook_Rotation rotates GameObjects (eyes) to follow other avatars and a random target. This component is synchronized between players.
Eye Blinking AvatarBlink_Simple randomly hides GameObjects (eyes) every few seconds, emulating a blink.
Example Avatar Prefab hierarchy
Offset Constraint OffsetConstraint allows to shift an object in relation to another one in Avatar space. This allows, for example, to have a Body follow the Head but keep rotation levelled. It also allows to construct simple neck models.
Limb Constraint BasicIKConstraint is a very minimalistic constraint that takes two transforms and a hint. This is useful to construct simple arm or leg chains. As rotation is currently not properly implemented, arms and legs may need to be rotationally symmetric to "look right". It's called "Basic" for a reason!
If you want to display different html content whether the client is using a regular browser or using AR or VR, you can just use a set of html classes. This is controlled via HTML element classes. For example, to make content appear on desktop and in AR add a <div class="desktop ar"> ... </div> inside the <needle-engine> tag:
<needle-engine>
+ <divclass="desktop ar"style="pointer-events:none;">
+ <divclass="positioning-container">
+ <p>your content for AR and desktop goes here</p>
+ <pclass="only-in-ar">This will only be visible in AR</p>
+ <div>
+ </div>
+</needle-engine>
+
Content Overlays are implemented using the optional dom-overlay feature which is usually supported on screen-based AR devices (phones, tablets).
`,1),$={href:"https://www.w3.org/TR/webxr-dom-overlays-1/#ua-style-sheet-defaults",target:"_blank",rel:"noopener noreferrer"},ee=e("em",null,"inside",-1),ne=e("code",null,'class="ar"',-1),te=e("h2",{id:"image-tracking",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#image-tracking","aria-hidden":"true"},"#"),n(" Image Tracking")],-1),ae={href:"https://github.com/immersive-web/marker-tracking/blob/main/explainer.md",target:"_blank",rel:"noopener noreferrer"},se=e("br",null,null,-1),oe=e("ul",null,[e("li",null,[n("Enable "),e("code",null,"WebXR Incubations"),n(" in chrome")]),e("li",null,[n("Add the "),e("code",null,"WebXRImageTracking"),n(" component")])],-1),re=e("p",null,[n("Without that spec, one can still request camera image access and run custom algorithms to determine device pose."),e("br"),n(" Libraries to add image tracking:")],-1),ie={href:"https://github.com/AR-js-org/AR.js",target:"_blank",rel:"noopener noreferrer"},le={href:"https://github.com/FireDragonGameStudio/NeedleAndARjs",target:"_blank",rel:"noopener noreferrer"},ce={href:"https://github.com/hiukim/mind-ar-js",target:"_blank",rel:"noopener noreferrer"},de=e("h2",{id:"augmented-reality-and-webxr-on-ios",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#augmented-reality-and-webxr-on-ios","aria-hidden":"true"},"#"),n(" Augmented Reality and WebXR on iOS")],-1),pe=e("p",null,"Augmented Reality experiences on iOS are somewhat limited, due to Apple currently not supporting WebXR on iOS devices.",-1),ue=e("h3",{id:"musical-instrument-webxr-and-quicklook-support",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#musical-instrument-webxr-and-quicklook-support","aria-hidden":"true"},"#"),n(" Musical Instrument โ WebXR and QuickLook support")],-1),he=e("p",null,"Here's an example for a musical instrument that uses Everywhere Actions and thus works in browsers and in AR on iOS devices. It uses spatial audio, animation, and tap interactions.",-1),me=e("h3",{id:"everywhere-actions-and-other-options-for-ios-ar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#everywhere-actions-and-other-options-for-ios-ar","aria-hidden":"true"},"#"),n(" Everywhere Actions and other options for iOS AR")],-1),ge=e("p",null,"There's also other options for guiding iOS users to even more capable interactive AR experiences:",-1),be={start:"3"},_e=e("strong",null,"Exporting content on-the-fly as USDZ files.",-1),fe=e("br",null,null,-1),ve={href:"https://castle.needle.tools",target:"_blank",rel:"noopener noreferrer"},ke={href:"https://accurate-tree-observation.glitch.me/",target:"_blank",rel:"noopener noreferrer"},ye=e("br",null,null,-1),we=e("strong",null,"Guiding users towards WebXR-compatible browsers on iOS.",-1),Re={href:"https://apps.apple.com/de/app/webxr-viewer/id1295998056",target:"_blank",rel:"noopener noreferrer"},Ae=e("p",null,[e("strong",null,"Using camera access and custom algorithms on iOS devices."),e("br"),n(" One can request camera image access and run custom algorithms to determine device pose."),e("br"),n(" While we currently don't provide built-in components for this, here's a few references to libraries and frameworks that we want to try in the future:")],-1),xe={href:"https://github.com/AR-js-org/AR.js",target:"_blank",rel:"noopener noreferrer"},Se={href:"https://github.com/FireDragonGameStudio/NeedleAndARjs",target:"_blank",rel:"noopener noreferrer"},Te={href:"https://github.com/hiukim/mind-ar-js",target:"_blank",rel:"noopener noreferrer"},Ee={href:"https://www.8thwall.com/",target:"_blank",rel:"noopener noreferrer"},je=e("h2",{id:"references",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#references","aria-hidden":"true"},"#"),n(" References")],-1),Oe={href:"https://www.w3.org/TR/webxr/",target:"_blank",rel:"noopener noreferrer"},qe=e("br",null,null,-1),We={href:"https://caniuse.com/webxr",target:"_blank",rel:"noopener noreferrer"},Ce=e("br",null,null,-1),Xe={href:"https://developer.apple.com/augmented-reality/quick-look/",target:"_blank",rel:"noopener noreferrer"};function Be(Ve,Me){const o=i("RouterLink"),c=i("kb"),a=i("ExternalLinkIcon"),l=i("sample");return p(),u("div",null,[m,e("table",null,[g,e("tbody",null,[b,_,f,e("tr",null,[v,k,e("td",null,[n("No full code support, but Needle "),t(o,{to:"/everywhere-actions.html"},{default:s(()=>[n("Everywhere Actions")]),_:1}),n(" are supported for creating dynamic, interactive USDZ files.")])]),y,w,R])]),A,e("p",null,[x,n(": Requires enabling a browser flag: "),S,T,E,n(": Requires enabling a toggle in the Developer settings"),j,O,n(": Uses "),t(o,{to:"/everywhere-actions.html"},{default:s(()=>[n("Everywhere Actions")]),_:1}),n(" or "),q]),W,C,X,e("ul",null,[B,e("li",null,[e("p",null,[V,M,n(" Pointing onto a surface and pressing "),t(c,null,{default:s(()=>[n("A")]),_:1}),n(" on a controller switches between 1:10 scale (miniature mode) and 1:1 scale."),N,n(" We're planning to add a component to have more control over this functionality.")])])]),P,e("blockquote",null,[e("p",null,[e("strong",null,[e("a",I,[n("Castle Builder"),t(a)])]),n(" uses all of the above for a cross-platform multiplayer sandbox experience."),D,n(" โ #madebyneedle ๐")])]),L,e("p",null,[n("Needle Engine supports the "),e("a",G,[H,t(a)]),n(" state. This allows users to seamlessly traverse between WebXR applications without leaving an immersive session โ they stay in VR or AR.")]),F,U,Q,e("p",null,[n("Read more about scripting for XR at the "),t(o,{to:"/scripting.html#xr-event-methods"},{default:s(()=>[n("scripting XR documentation")]),_:1})]),z,e("p",null,[n("Use the "),K,n(" class to show/hide specific content while in AR. The "),e("a",Y,[Z,n(" pseudo class"),t(a)]),n(" shouldn't be used at this point because using it breaks Mozilla's WebXR Viewer.")]),J,e("p",null,[n("It's worth noting that the overlay element "),e("a",$,[n("will be always displayed fullscreen while in XR"),t(a)]),n(", independent of styling that has been applied. If you want to align items differently, you should make a container "),ee,n(" the "),ne,n(" element.")]),t(l,{src:"https://engine.needle.tools/samples-uploads/ar-overlay/"}),te,e("p",null,[n('WebXR ImageTracking is still in "draft" phase: '),e("a",ae,[n("Marker Tracking Explainer"),t(a)]),se,n(" But you can still use WebXR ImageTracking with Needle Engine today:")]),oe,e("p",null,[n("You can find additional documentation in the "),t(o,{to:"/everywhere-actions.html#image-tracking"},{default:s(()=>[n("Everywhere Actions")]),_:1}),n(" section")]),re,e("ul",null,[e("li",null,[e("a",ie,[n("AR.js"),t(a)]),n(" (open source) "),e("ul",null,[e("li",null,[e("a",le,[n("Experimental AR.js integration"),t(a)]),n(" by FireDragonGameStudio")])])]),e("li",null,[e("a",ce,[n("Mind AR"),t(a)]),n(" (open source)")])]),de,pe,e("p",null,[n("Needle Engine's "),t(o,{to:"/everywhere-actions.html"},{default:s(()=>[n("Everywhere Actions")]),_:1}),n(" are designed to fill that gap, bringing automatic interactive capabilities to iOS devices for scenes composed of specific components. They support a subset of the functionality that's available in WebXR, for example spatial audio, image tracking, animations, and more. See "),t(o,{to:"/everywhere-actions.html"},{default:s(()=>[n("the docs")]),_:1}),n(" for more information.")]),ue,he,t(l,{src:"https://engine.needle.tools/samples-uploads/musical-instrument"}),me,ge,e("ol",be,[e("li",null,[_e,fe,n(" These files can be displayed on iOS devices in AR. When exported from scenes with Everywhere Actions the interactivity is the same, more than sufficient for product configurators, narrative experiences and similar. An example is "),e("a",ve,[n("Castle Builder"),t(a)]),n(" where creations (not the live session) can be viewed in AR.")])]),e("blockquote",null,[e("p",null,[e("strong",null,[e("a",ke,[n("Encryption in Space"),t(a)])]),n(" uses this approach. Players can collaboratively place text into the scene on their screens and then view the results in AR on iOS. On Android, they can also interact right in WebXR."),ye,n(" โ #madewithneedle by Katja Rempel ๐")])]),e("ol",null,[e("li",null,[e("p",null,[we,n(" Depending on your target audience, you can guide users on iOS towards for example Mozilla's "),e("a",Re,[n("WebXR Viewer"),t(a)]),n(" to experience AR on iOS.")])]),e("li",null,[Ae,e("ul",null,[e("li",null,[e("a",xe,[n("AR.js"),t(a)]),n(" (open source) "),e("ul",null,[e("li",null,[e("a",Se,[n("Experimental AR.js integration"),t(a)]),n(" by FireDragonGameStudio")])])]),e("li",null,[e("a",Te,[n("Mind AR"),t(a)]),n(" (open source)")]),e("li",null,[e("a",Ee,[n("8th Wall"),t(a)]),n(" (commercial)")])])])]),je,e("p",null,[e("a",Oe,[n("WebXR Device API"),t(a)]),qe,e("a",We,[n("caniuse: WebXR"),t(a)]),Ce,e("a",Xe,[n("Apple's Preliminary USD Behaviours"),t(a)])])])}const Pe=d(h,[["render",Be],["__file","xr.html.vue"]]);export{Pe as default};
diff --git a/assets/xr.html-c065dc72.js b/assets/xr.html-c065dc72.js
new file mode 100644
index 000000000..8a86d3ce3
--- /dev/null
+++ b/assets/xr.html-c065dc72.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-099a1df4","path":"/xr.html","title":"VR & AR (WebXR)","lang":"en-US","frontmatter":{"title":"VR & AR (WebXR)","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/vr & ar.png"}],["meta",{"name":"og:description","content":"---\\nTheoretically all WebXR-capable devices and browsers should be supported. That being said, we've tested the following configurations:"}]],"description":"---\\nTheoretically all WebXR-capable devices and browsers should be supported. That being said, we've tested the following configurations:"},"headers":[{"level":2,"title":"Supported Devices","slug":"supported-devices","link":"#supported-devices","children":[]},{"level":2,"title":"Adding VR and AR capabilities to a scene","slug":"adding-vr-and-ar-capabilities-to-a-scene","link":"#adding-vr-and-ar-capabilities-to-a-scene","children":[{"level":3,"title":"Basic capabilities","slug":"basic-capabilities","link":"#basic-capabilities","children":[]},{"level":3,"title":"Multiplayer","slug":"multiplayer","link":"#multiplayer","children":[]},{"level":3,"title":"Special AR Components","slug":"special-ar-components","link":"#special-ar-components","children":[]},{"level":3,"title":"Controlling object display for XR","slug":"controlling-object-display-for-xr","link":"#controlling-object-display-for-xr","children":[]},{"level":3,"title":"Travelling between VR worlds","slug":"travelling-between-vr-worlds","link":"#travelling-between-vr-worlds","children":[]}]},{"level":2,"title":"Scripting","slug":"scripting","link":"#scripting","children":[]},{"level":2,"title":"Avatars","slug":"avatars","link":"#avatars","children":[{"level":3,"title":"Experimental Avatar Components","slug":"experimental-avatar-components","link":"#experimental-avatar-components","children":[]}]},{"level":2,"title":"HTML Content Overlays in AR","slug":"html-content-overlays-in-ar","link":"#html-content-overlays-in-ar","children":[]},{"level":2,"title":"Image Tracking","slug":"image-tracking","link":"#image-tracking","children":[]},{"level":2,"title":"Augmented Reality and WebXR on iOS","slug":"augmented-reality-and-webxr-on-ios","link":"#augmented-reality-and-webxr-on-ios","children":[{"level":3,"title":"Musical Instrument โ WebXR and QuickLook support","slug":"musical-instrument-webxr-and-quicklook-support","link":"#musical-instrument-webxr-and-quicklook-support","children":[]},{"level":3,"title":"Everywhere Actions and other options for iOS AR","slug":"everywhere-actions-and-other-options-for-ios-ar","link":"#everywhere-actions-and-other-options-for-ios-ar","children":[]}]},{"level":2,"title":"References","slug":"references","link":"#references","children":[]}],"git":{},"filePathRelative":"xr.md"}`);export{e as data};
diff --git a/backlog-mermaid.html b/backlog-mermaid.html
new file mode 100644
index 000000000..324622614
--- /dev/null
+++ b/backlog-mermaid.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+ Needle Engine Documentation
+
+
+
+
+
Generated Projects can either be added to source control or kept dynamic. Adding them to source control unlocks being able to adjust HTML, CSS, etc very flexible. To generate dynamic projects, change their path to ../Library/MyScene. They will be regenerated if needed.
Please follow the instructions in the Authentication section if this is your first time accessing packages by needle on this machine.
Make sure you have a Needle Engine and Exporter license, otherwise the following steps will fail (you'll not be able to get authenticated package access).
Needs to be setup once per machine.
Clone this repository and open starter/Authenticate with Unity 2020.3.x
Thank you for using Needle Engine for Blender Alpha. With this addon you can create highly interactive and optimized WebGL and WebXR experiences inside Blender that run using Needle Engine and three.js. You'll be able to sequence animations, easily lightmap your scenes, add interactivity or create your own scripts written in Typescript or Javascript that run on the web. You own your content!
Automatically export HDRI environment lights directly from blender. Save to reload your local server
Please note: The current state of the exporter for Blender is in the alpha phase - which means that some features you may know from the Needle Engine Unity Integration may not yet be implemented. Your feedback is invaluable when it comes to deciding which of those features should be prioritizes. If you have feedback for us please let us know in discussionsopen in new window or in our discord communityopen in new window!
The Blender addon is downloaded as a zip file. In Blender go to File / Settings / Add-ons and click the Install button. Then select the downloaded zip to install it.
First create or open a new blend file that you want to be exported to the web. Open the Properties window open the scene category. Select a Project Path in the Needle Engine panel. Then click Generate Project. It will automatically install and start the server - once it has finished your browser should open and the threejs scene will load.
By default your scene will automatically re-exported when you save the blend file. If the local server is running (e.g. by clicking Run Project) the website will automatically refresh with your changed model.
When your web project already exists and you just want to continue working on the website click the blue Run Project button to start the local server:
The path to your web project. You can use the little folder button on the right to select a different path.
The Run Project button shows up when the Project path shows to a valid web project. A web project is valid when it contains a package.json
Directory open the directory of your web project (the Project Path)
This button re-exports the current scene as a glb to your local web project. This also happens by default when saving your blend file.
Code Editor tries to open the vscode workspace in your web project
If you work with multiple scenes in one blend file you can configure which scene is your "main" scene and should be exported to the web. If any of your components references another scene they will also be exported as separate glb files.
Use the Build: Development or Build: Production buttons when you want to upload your web project to a server. This will bundle your web project and produce the files that you can upload. When clicking Build: Production it will also apply optimization to your textures (they will be compressed for the web)
By default the blender viewport is set to Filmic - with this setting your colors in Blender and in three.js will not match. To fix this go to the Blender Render category and in the ColorManagement panel select View Transform: Standard
For simple usecases you can use the Animation component for playback of one or multiple animationclips. Just select your object, add an Animation component and assign the clip (you can add additional clips to be exported to the clips array. By default it will only playback the first clip assigned when playAutomatically is enabled. You can trigger the other clips using a simple custom typescript component)
The animator controller can be created for more complex scenarios. It works as a statemachine which allows you to create multiple animation states in a graph and configure conditions and interpolation settings for transitioning between those.
The Parameters node will be created once you add a first node. Select it to setup parameters to be used in transitions (via the Node panel on the right border)
This is an AnimatorState. the orange state is the start state (it can be changed using the Set default state button in the Node/Properties panel)
The Properties for an AnimatorState can be used to setup one or multiple transitions to other states. Use the Conditions array to select parameters that must match the condition for doing the transition.
To use an AnimatorController add an Animator component to the root object of your animations and select the AnimatorController asset that you want to use for this object.
You can set the Animator parameters from typescript or by e.g. using the event of a Button component:
Exporting Blender nla tracks to threejs. Add a PlayableDirector component (via Add Component) to a any blender object. Assign the objects in the animation tracks list in the component for which you want the nla tracks to be exported.
Code example for interactive timeline playback
Add this script to src/scripts (see custom components section) and add it to any object in Blender to make a timeline's time be controlled by scrolling in the browsers
You can add or remove components to objects in your hierarchy using the Needle Components panel:
For example by adding an OrbitControls component to the camera object you get basic camera controls for mobile and desktop devicesAdjust settings for each component in their respective panels
Components can be removed using the X button in the lower right:
Custom components can also be easily added by simply writing Typescript classes. They will automatically compile and show up in Blender when saved.
To create custom components open the workspace via the Needle Project panel and add a .ts script file in src/scripts inside your web project. Please refer to the scripting documentationopen in new window to learn how to write custom components for Needle Engine.
Note
Make sure @needle-tools/needle-component-compiler 2.x is installed in your web project (package.json devDependencies)
Needle Lightmapping will automatically generate lightmap UVs for all models marked to be lightmapped. For lightmapping to work you need at least one light and one object with Lightmapped turned on.
Please keep in mind:
You are using an early preview of these features - we recommend creating a backup of your blend file when using Lightmapping at this point in time. Please report problems or errors you encounter in our discordopen in new window ๐
The Needle Engine Build Pipeline automatically compresses textures using ECT1S and UASTC (depending on their usage in materials) when making a production build (requires toktx being installed). But you can override or change the compression type per texture in the Material panel.
You can modify the compression that is being applied per texture. To override the default compression settings go to the Material tab and open the Needle Material Settings. There you will find a toggle to override the texture settings per texture used in your material. See the texture compression table for a brief overview over the differences between each compression algorithm.
You can also automatically create and upload a bugreport directly from Blender (this currently requires a node.js web project being setup). Uploaded bugreports will solely used for debugging, they are encrypted on our backend and will deleted after 30 days.
+
+
+
diff --git a/blender/lightmapping-object.webp b/blender/lightmapping-object.webp
new file mode 100644
index 000000000..822c765c4
Binary files /dev/null and b/blender/lightmapping-object.webp differ
diff --git a/blender/lightmapping-panel.webp b/blender/lightmapping-panel.webp
new file mode 100644
index 000000000..cc265138c
Binary files /dev/null and b/blender/lightmapping-panel.webp differ
diff --git a/blender/lightmapping-scene-panel.webp b/blender/lightmapping-scene-panel.webp
new file mode 100644
index 000000000..a6f145989
Binary files /dev/null and b/blender/lightmapping-scene-panel.webp differ
diff --git a/blender/lightmapping.mp4 b/blender/lightmapping.mp4
new file mode 100644
index 000000000..a25642d63
Binary files /dev/null and b/blender/lightmapping.mp4 differ
diff --git a/blender/logo.png b/blender/logo.png
new file mode 100644
index 000000000..8f04e2756
Binary files /dev/null and b/blender/logo.png differ
diff --git a/blender/object-panels.webp b/blender/object-panels.webp
new file mode 100644
index 000000000..63a533f26
Binary files /dev/null and b/blender/object-panels.webp differ
diff --git a/blender/project-panel-2.webp b/blender/project-panel-2.webp
new file mode 100644
index 000000000..8a8745c48
Binary files /dev/null and b/blender/project-panel-2.webp differ
diff --git a/blender/project-panel-3.webp b/blender/project-panel-3.webp
new file mode 100644
index 000000000..e1fe1c80f
Binary files /dev/null and b/blender/project-panel-3.webp differ
diff --git a/blender/project-panel.webp b/blender/project-panel.webp
new file mode 100644
index 000000000..768f411d0
Binary files /dev/null and b/blender/project-panel.webp differ
diff --git a/blender/remove-component.webp b/blender/remove-component.webp
new file mode 100644
index 000000000..58191b706
Binary files /dev/null and b/blender/remove-component.webp differ
diff --git a/blender/settings-color-management.webp b/blender/settings-color-management.webp
new file mode 100644
index 000000000..3cb381249
Binary files /dev/null and b/blender/settings-color-management.webp differ
diff --git a/blender/settings.webp b/blender/settings.webp
new file mode 100644
index 000000000..3e7166b68
Binary files /dev/null and b/blender/settings.webp differ
diff --git a/blender/texture-compression.webp b/blender/texture-compression.webp
new file mode 100644
index 000000000..7a08a6ed6
Binary files /dev/null and b/blender/texture-compression.webp differ
diff --git a/blender/timeline.webp b/blender/timeline.webp
new file mode 100644
index 000000000..1362d3808
Binary files /dev/null and b/blender/timeline.webp differ
diff --git a/blender/timeline_setup.webp b/blender/timeline_setup.webp
new file mode 100644
index 000000000..6771cd7e8
Binary files /dev/null and b/blender/timeline_setup.webp differ
diff --git a/blender/updates.webp b/blender/updates.webp
new file mode 100644
index 000000000..8dc20e608
Binary files /dev/null and b/blender/updates.webp differ
diff --git a/castle.png b/castle.png
new file mode 100644
index 000000000..8e0f89573
Binary files /dev/null and b/castle.png differ
diff --git a/code-samples/basic-component.ts b/code-samples/basic-component.ts
new file mode 100644
index 000000000..ff1cb9931
--- /dev/null
+++ b/code-samples/basic-component.ts
@@ -0,0 +1,16 @@
+import { Behaviour, serializable } from "@needle-tools/engine"
+import { Object3D } from "three"
+
+export class MyComponent extends Behaviour {
+
+ @serializable(Object3D)
+ myObjectReference?: Object3D;
+
+ start() {
+ console.log("Hello world", this);
+ }
+
+ update() {
+ this.gameObject.rotateY(this.context.time.deltaTime);
+ }
+}
\ No newline at end of file
diff --git a/code-samples/basic-html.html b/code-samples/basic-html.html
new file mode 100644
index 000000000..ff5096f90
--- /dev/null
+++ b/code-samples/basic-html.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+ Made with Needle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/code-samples/component-2d-audio.ts b/code-samples/component-2d-audio.ts
new file mode 100644
index 000000000..bea8ddd81
--- /dev/null
+++ b/code-samples/component-2d-audio.ts
@@ -0,0 +1,22 @@
+import { AudioSource, Behaviour, serializable } from "@needle-tools/engine";
+
+// declaring AudioClip type is for codegen to produce the correct input field (for e.g. Unity or Blender)
+declare type AudioClip = string;
+
+export class My2DAudio extends Behaviour {
+
+ // The clip contains a string pointing to the audio file - by default it's relative to the GLB that contains the component
+ // by adding the URL decorator the clip string will be resolved relative to your project root and can be loaded
+ @serializable(URL)
+ clip?: AudioClip;
+
+ awake() {
+ // creating a new audio element and playing it
+ const audioElement = new Audio(this.clip);
+ audioElement.loop = true;
+ // on the web we have to wait for the user to interact with the page before we can play audio
+ AudioSource.registerWaitForAllowAudio(() => {
+ audioElement.play();
+ })
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-animation-onclick.ts b/code-samples/component-animation-onclick.ts
new file mode 100644
index 000000000..365d684fe
--- /dev/null
+++ b/code-samples/component-animation-onclick.ts
@@ -0,0 +1,20 @@
+import { Behaviour, serializable, Animation, IPointerClickHandler, PointerEventData } from "@needle-tools/engine";
+
+export class PlayAnimationOnClick extends Behaviour implements IPointerClickHandler {
+
+ @serializable(Animation)
+ animation?: Animation;
+
+ awake() {
+ if (this.animation) {
+ this.animation.playAutomatically = false;
+ this.animation.loop = false;
+ }
+ }
+
+ onPointerClick(_args: PointerEventData) {
+ if (this.animation) {
+ this.animation.play();
+ }
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-animationclip.ts b/code-samples/component-animationclip.ts
new file mode 100644
index 000000000..54be3cb12
--- /dev/null
+++ b/code-samples/component-animationclip.ts
@@ -0,0 +1,12 @@
+import { Behaviour, serializable } from "@needle-tools/engine";
+import { AnimationClip } from "three"
+
+export class ExportAnimationClip extends Behaviour {
+
+ @serializable(AnimationClip)
+ animation?: AnimationClip;
+
+ awake() {
+ console.log("My referenced animation clip", this.animation);
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-click-networking.ts b/code-samples/component-click-networking.ts
new file mode 100644
index 000000000..5ca54dd62
--- /dev/null
+++ b/code-samples/component-click-networking.ts
@@ -0,0 +1,27 @@
+import { Behaviour, EventList, IPointerClickHandler, PointerEventData, serializable } from "@needle-tools/engine";
+
+export class SyncedClick extends Behaviour implements IPointerClickHandler {
+
+ @serializable(EventList)
+ onClick!: EventList;
+
+ onPointerClick(_args: PointerEventData) {
+ console.log("SEND CLICK");
+ this.context.connection.send("clicked/" + this.guid);
+ this.onClick?.invoke();
+ }
+
+ onEnable(): void {
+ this.context.connection.beginListen("clicked/" + this.guid, this.onRemoteClick);
+ }
+ onDisable(): void {
+ this.context.connection.stopListen("clicked/" + this.guid, this.onRemoteClick);
+ }
+
+
+ onRemoteClick = () => {
+ console.log("RECEIVED CLICK");
+ this.onClick?.invoke();
+ }
+
+}
\ No newline at end of file
diff --git a/code-samples/component-click.ts b/code-samples/component-click.ts
new file mode 100644
index 000000000..d90e185cd
--- /dev/null
+++ b/code-samples/component-click.ts
@@ -0,0 +1,9 @@
+import { Behaviour, IPointerClickHandler, PointerEventData, showBalloonMessage } from "@needle-tools/engine";
+
+export class ClickExample extends Behaviour implements IPointerClickHandler {
+
+ // Make sure to have an ObjectRaycaster component in the parent hierarchy
+ onPointerClick(_args: PointerEventData) {
+ showBalloonMessage("Clicked " + this.name);
+ }
+}
diff --git a/code-samples/component-customevent.ts b/code-samples/component-customevent.ts
new file mode 100644
index 000000000..b6bb933bf
--- /dev/null
+++ b/code-samples/component-customevent.ts
@@ -0,0 +1,42 @@
+import { Behaviour, serializable, EventList } from "@needle-tools/engine";
+import { Object3D } from "three";
+
+/*
+Make sure to have a c# file in your project with the following content:
+
+using UnityEngine;
+using UnityEngine.Events;
+
+[System.Serializable]
+public class MyCustomUnityEvent : UnityEvent
+{
+}
+
+Unity documentation about custom events:
+https://docs.unity3d.com/ScriptReference/Events.UnityEvent_2.html
+
+*/
+
+// Documentation โ https://docs.needle.tools/scripting
+
+export class CustomEventCaller extends Behaviour {
+
+ // The next line is not just a comment, it defines
+ // a specific type for the component generator to use.
+
+ //@type MyCustomUnityEvent
+ @serializable(EventList)
+ myEvent!: EventList;
+
+ // just for testing - could be when a button is clicked, etc.
+ start() {
+ this.myEvent.invoke("Hello");
+ }
+}
+
+export class CustomEventReceiver extends Behaviour {
+
+ logStringAndObject(str: string) {
+ console.log("From Event: ", str);
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-customshaderproperty.ts b/code-samples/component-customshaderproperty.ts
new file mode 100644
index 000000000..949e304d8
--- /dev/null
+++ b/code-samples/component-customshaderproperty.ts
@@ -0,0 +1,20 @@
+import { Behaviour, serializable } from "@needle-tools/engine";
+import { Material } from "three";
+
+declare type MyCustomShaderMaterial = Material & {
+ _Speed: number;
+};
+
+export class IncreaseShaderSpeedOverTime extends Behaviour {
+
+ //@type UnityEngine.Material
+ @serializable(Material)
+ myMaterial?: MyCustomShaderMaterial;
+
+ update() {
+ if (this.myMaterial) {
+ this.myMaterial._Speed *= 1 + this.context.time.deltaTime;
+ if(this.myMaterial._Speed > 1) this.myMaterial._Speed = .0005;
+ }
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-everywhere-action-hideonstart.ts b/code-samples/component-everywhere-action-hideonstart.ts
new file mode 100644
index 000000000..0c471ee28
--- /dev/null
+++ b/code-samples/component-everywhere-action-hideonstart.ts
@@ -0,0 +1,22 @@
+export class HideOnStart extends Behaviour implements UsdzBehaviour {
+
+ start() {
+ this.gameObject.visible = false;
+ }
+
+ createBehaviours(ext, model, _context) {
+ if (model.uuid === this.gameObject.uuid)
+ ext.addBehavior(new BehaviorModel("HideOnStart_" + this.gameObject.name,
+ TriggerBuilder.sceneStartTrigger(),
+ ActionBuilder.fadeAction(model, 0, false)
+ ));
+ }
+
+ beforeCreateDocument() {
+ this.gameObject.visible = true;
+ }
+
+ afterCreateDocument() {
+ this.gameObject.visible = false;
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-filereference.ts b/code-samples/component-filereference.ts
new file mode 100644
index 000000000..387ba54eb
--- /dev/null
+++ b/code-samples/component-filereference.ts
@@ -0,0 +1,20 @@
+import { Behaviour, FileReference, ImageReference, serializable } from "@needle-tools/engine";
+
+export class FileReferenceExample extends Behaviour {
+
+ // A FileReference can be used to load and assign arbitrary data in the editor. You can use it to load images, audio, text files... FileReference types will not be saved inside as part of the GLB (the GLB will only contain a relative URL to the file)
+ @serializable(FileReference)
+ myFile?: FileReference;
+ // Tip: if you want to export and load an image (that is not part of your GLB) if you intent to add it to your HTML content for example you can use the ImageReference type instead of FileReference. It will be loaded as an image and you can use it as a source for an tag.
+
+ async start() {
+ console.log("This is my file: ", this.myFile);
+ // load the file
+ const data = await this.myFile?.loadRaw();
+ if (!data) {
+ console.error("Failed loading my file...");
+ return;
+ }
+ console.log("Loaded my file. These are the bytes:", await data.arrayBuffer());
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-location.ts b/code-samples/component-location.ts
new file mode 100644
index 000000000..bb95f392b
--- /dev/null
+++ b/code-samples/component-location.ts
@@ -0,0 +1,11 @@
+import { Behaviour, showBalloonMessage } from "@needle-tools/engine";
+
+export class WhereAmI extends Behaviour {
+ start() {
+ navigator.geolocation.getCurrentPosition((position) => {
+ console.log("Navigator response:", position);
+ const latlong = position.coords.latitude + ", " + position.coords.longitude;
+ showBalloonMessage("You are at\nLatLong " + latlong);
+ });
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-nested-serialization-cs.cs b/code-samples/component-nested-serialization-cs.cs
new file mode 100644
index 000000000..445a11183
--- /dev/null
+++ b/code-samples/component-nested-serialization-cs.cs
@@ -0,0 +1,17 @@
+using System;
+
+[Serializable]
+public class CustomSubData
+{
+ public string subString;
+ public float subNumber;
+}
+
+[Serializable]
+public class CustomData
+{
+ public string myStringField;
+ public float myNumberField;
+ public bool myBooleanField;
+ public CustomSubData subData;
+}
\ No newline at end of file
diff --git a/code-samples/component-nested-serialization.ts b/code-samples/component-nested-serialization.ts
new file mode 100644
index 000000000..2a3a4473c
--- /dev/null
+++ b/code-samples/component-nested-serialization.ts
@@ -0,0 +1,40 @@
+import { Behaviour, serializable } from "@needle-tools/engine";
+
+// Documentation โ https://docs.needle.tools/scripting
+
+class CustomSubData {
+ @serializable()
+ subString: string = "";
+
+ @serializable()
+ subNumber: number = 0;
+}
+
+class CustomData {
+ @serializable()
+ myStringField: string = "";
+
+ @serializable()
+ myNumberField: number = 0;
+
+ @serializable()
+ myBooleanField: boolean = false;
+
+ @serializable(CustomSubData)
+ subData: CustomSubData | undefined = undefined;
+
+ someMethod() {
+ console.log("My string is " + this.myStringField, "my sub data", this.subData)
+ }
+}
+
+export class SerializedDataSample extends Behaviour {
+
+ @serializable(CustomData)
+ myData: CustomData | undefined;
+
+ onEnable() {
+ console.log(this.myData);
+ this.myData?.someMethod();
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-object-reference.ts b/code-samples/component-object-reference.ts
new file mode 100644
index 000000000..f3a94e49c
--- /dev/null
+++ b/code-samples/component-object-reference.ts
@@ -0,0 +1,13 @@
+import { Behaviour, serializable } from "@needle-tools/engine";
+import { Object3D } from "three"
+
+export class MyClass extends Behaviour {
+ // this will be a "Transform" field in Unity
+ @serializable(Object3D)
+ myObjectReference: Object3D | null = null;
+
+ // this will be a "Transform" array field in Unity
+ // Note that the @serializable decorator contains the array content type! (Object3D and not Object3D[])
+ @serializable(Object3D)
+ myObjectReferenceList: Object3D[] | null = null;
+}
\ No newline at end of file
diff --git a/code-samples/component-prefab.ts b/code-samples/component-prefab.ts
new file mode 100644
index 000000000..73f5551ee
--- /dev/null
+++ b/code-samples/component-prefab.ts
@@ -0,0 +1,20 @@
+import { Behaviour, serializable, AssetReference } from "@needle-tools/engine";
+
+export class MyClass extends Behaviour {
+
+ // if you export a prefab or scene as a reference from Unity you'll get a path to that asset
+ // which you can de-serialize to AssetReference for convenient loading
+ @serializable(AssetReference)
+ myPrefab?: AssetReference;
+
+ async start() {
+ // directly instantiate
+ const myInstance = await this.myPrefab?.instantiate();
+
+ // you can also just load and instantiate later
+ // const myInstance = await this.myPrefab.loadAssetAsync();
+ // this.gameObject.add(myInstance)
+ // this is useful if you know that you want to load this asset only once because it will not create a copy
+ // since ``instantiate()`` does create a copy of the asset after loading it
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-scene.ts b/code-samples/component-scene.ts
new file mode 100644
index 000000000..38fdfa048
--- /dev/null
+++ b/code-samples/component-scene.ts
@@ -0,0 +1,34 @@
+import { Behaviour, serializable, AssetReference } from "@needle-tools/engine";
+
+export class LoadingScenes extends Behaviour {
+ // tell the component compiler that we want to reference an array of SceneAssets
+ // @type UnityEditor.SceneAsset[]
+ @serializable(AssetReference)
+ myScenes?: AssetReference[];
+
+ async awake() {
+ if (!this.myScenes) {
+ return;
+ }
+ for (const scene of this.myScenes) {
+ // check if it is assigned in unity
+ if(!scene) continue;
+ // load the scene once
+ const myScene = await scene.loadAssetAsync();
+ // add it to the threejs scene
+ this.gameObject.add(myScene);
+
+ // of course you can always just load one at a time
+ // and remove it from the scene when you want
+ // myScene.removeFromParent();
+ // this is the same as scene.asset.removeFromParent()
+ }
+ }
+
+ onDestroy(): void {
+ if (!this.myScenes) return;
+ for (const scene of this.myScenes) {
+ scene?.unload();
+ }
+ }
+}
\ No newline at end of file
diff --git a/code-samples/component-time.ts b/code-samples/component-time.ts
new file mode 100644
index 000000000..9c7e8431a
--- /dev/null
+++ b/code-samples/component-time.ts
@@ -0,0 +1,21 @@
+import { Behaviour, Text, serializable, WaitForSeconds } from "@needle-tools/engine";
+
+export class DisplayTime extends Behaviour {
+
+ @serializable(Text)
+ text?: Text;
+
+ onEnable(): void {
+ this.startCoroutine(this.updateTime())
+ }
+
+ private *updateTime() {
+ while (true) {
+ if (this.text) {
+ this.text.text = new Date().toLocaleTimeString();
+ console.log(this.text.text)
+ }
+ yield WaitForSeconds(1)
+ }
+ };
+}
\ No newline at end of file
diff --git a/code-samples/component-unityevent.ts b/code-samples/component-unityevent.ts
new file mode 100644
index 000000000..3f326a790
--- /dev/null
+++ b/code-samples/component-unityevent.ts
@@ -0,0 +1,11 @@
+import { Behaviour, serializable, EventList } from "@needle-tools/engine"
+
+export class MyComponent extends Behaviour {
+
+ @serializable(EventList)
+ myEvent? : EventList;
+
+ start() {
+ this.myEvent?.invoke();
+ }
+}
\ No newline at end of file
diff --git a/code-samples/custom-particle-system-behaviour.ts b/code-samples/custom-particle-system-behaviour.ts
new file mode 100644
index 000000000..f12b58eb0
--- /dev/null
+++ b/code-samples/custom-particle-system-behaviour.ts
@@ -0,0 +1,17 @@
+import { Behaviour, ParticleSystem } from "@needle-tools/engine";
+import { ParticleSystemBaseBehaviour, QParticle } from "@needle-tools/engine";
+
+// Derive your custom behaviour from the ParticleSystemBaseBehaviour class (or use QParticleBehaviour)
+class MyParticlesBehaviour extends ParticleSystemBaseBehaviour {
+
+ // callback invoked per particle
+ update(particle: QParticle): void {
+ particle.position.y += 5 * this.context.time.deltaTime;
+ }
+}
+export class TestCustomParticleSystemBehaviour extends Behaviour {
+ start() {
+ // add your custom behaviour to the particle system
+ this.gameObject.getComponent(ParticleSystem)!.addBehaviour(new MyParticlesBehaviour())
+ }
+}
diff --git a/code-samples/custom-post-effect.ts b/code-samples/custom-post-effect.ts
new file mode 100644
index 000000000..3d6c6cfc5
--- /dev/null
+++ b/code-samples/custom-post-effect.ts
@@ -0,0 +1,44 @@
+import { EffectProviderResult, PostProcessingEffect, registerCustomEffectType, serializable } from "@needle-tools/engine";
+import { OutlineEffect } from "postprocessing";
+import { Object3D } from "three";
+
+export class OutlinePostEffect extends PostProcessingEffect {
+
+ // the outline effect takes a list of objects to outline
+ @serializable(Object3D)
+ selection!: Object3D[];
+
+ // this is just an example method that you could call to update the outline effect selection
+ updateSelection() {
+ if (this._outlineEffect) {
+ this._outlineEffect.selection.clear();
+ for (const obj of this.selection) {
+ this._outlineEffect.selection.add(obj);
+ }
+ }
+ }
+
+
+ // a unique name is required for custom effects
+ get typeName(): string {
+ return "Outline";
+ }
+
+ private _outlineEffect: void | undefined | OutlineEffect;
+
+ // method that creates the effect once
+ onCreateEffect(): EffectProviderResult | undefined {
+
+ const outlineEffect = new OutlineEffect(this.context.scene, this.context.mainCamera!);
+ this._outlineEffect = outlineEffect;
+ outlineEffect.edgeStrength = 10;
+ outlineEffect.visibleEdgeColor.set(0xff0000);
+ for (const obj of this.selection) {
+ outlineEffect.selection.add(obj);
+ }
+
+ return outlineEffect;
+ }
+}
+// You need to register your effect type with the engine
+registerCustomEffectType("Outline", OutlinePostEffect);
\ No newline at end of file
diff --git a/community/contributions/index.html b/community/contributions/index.html
new file mode 100644
index 000000000..64ff12d5f
--- /dev/null
+++ b/community/contributions/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+ Needle Engine Documentation
+
+
+
+
+
Add this class to your project to always open with Chrome instead of your default browser (Firefox in my case) when you click "Play" or "Start Server". Note: This is an editor class and should either be put into an editor-only assembly or wrapped in #if UNITY_EDITOR and #endif.
Add this class to your project to always open with Chrome instead of your default browser (Firefox in my case) when you click "Play" or "Start Server". Note: This is an editor class and should either be put into an editor-only assembly or wrapped in #if UNITY_EDITOR and #endif.
If you want to set a fallback material for an object that will be exported as USDZ (for AR-mode on iOS), you can add this script to the object, which material should be replaced.
This is especially useful if you use custom shaders in your scene (they are visible on Desktop+WebXR, but not in AR on iOS).
If you want to set a fallback material for an object that will be exported as USDZ (for AR-mode on iOS), you can add this script to the object, which material should be replaced.
This is especially useful if you use custom shaders in your scene (they are visible on Desktop+WebXR, but not in AR on iOS).
This is mostly a basic example on how to contribute. It will then be added on our documentation contributions page: https://engine.needle.tools/docs/community/contributions
Please include at least one code snippet, for example like this:
Use the mouse wheel or touch delta to update a timeline's time.
import{ Behaviour, PlayableDirector, serializeable }from"@needle-tools/engine";
+import{ Mathf }from"@needle-tools/engine";
+
+// Example of setting a timeline's time
+// without relying on any HTML elements.
+// Here we directly use the mousewheel scroll and the touch delta
+
+exportclassScrollTimeline_2extendsBehaviour{
+
+ @serializeable(PlayableDirector)
+ timeline?: PlayableDirector;
+
+ @serializeable()
+ scrollSpeed:number=0.5;
+
+ @serializeable()
+ lerpSpeed:number=2.5;
+
+ private targetTime:number=0;
+
+ start(){
+
+ this.timeline?.pause();
+
+ // Grab the mousewheel event
+ window.addEventListener("wheel",(evt: WheelEvent)=>this.updateTime(evt.deltaY));
+
+ // Touch events are a bit more complicated
+ // We need to keep track of the last touch position
+ // and calculate the delta between the current and the last position
+ let lastTouchPosition =-1;
+ window.addEventListener("touchmove",(evt: TouchEvent)=>{
+ const delta = evt.touches[0].clientY - lastTouchPosition;
+ // We only want to apply the delta if it's not TOO big
+ // e.g. when the user is scrolling the page
+ if(delta <10)this.updateTime(-delta);
+ // Update the last touch position
+ lastTouchPosition = evt.touches[0].clientY;
+ });
+ }
+
+ privateupdateTime(delta){
+ if(!this.timeline)return;
+ this.targetTime += delta *0.01*this.scrollSpeed;
+ this.targetTime = Mathf.clamp(this.targetTime,0,this.timeline.duration);
+ }
+
+ onBeforeRender():void{
+ if(!this.timeline)return;
+ this.timeline.pause();
+ this.timeline.time = Mathf.lerp(this.timeline.time,this.targetTime,this.lerpSpeed *this.context.time.deltaTime);
+ this.timeline.evaluate();
+ }
+}
+
+
Use the mouse wheel or touch delta to update a timeline's time.
import{ Behaviour, PlayableDirector, serializeable }from"@needle-tools/engine";
+import{ Mathf }from"@needle-tools/engine";
+
+// Example of setting a timeline's time
+// without relying on any HTML elements.
+// Here we directly use the mousewheel scroll and the touch delta
+
+exportclassScrollTimeline_2extendsBehaviour{
+
+ @serializeable(PlayableDirector)
+ timeline?: PlayableDirector;
+
+ @serializeable()
+ scrollSpeed:number=0.5;
+
+ @serializeable()
+ lerpSpeed:number=2.5;
+
+ private targetTime:number=0;
+
+ start(){
+
+ this.timeline?.pause();
+
+ // Grab the mousewheel event
+ window.addEventListener("wheel",(evt: WheelEvent)=>this.updateTime(evt.deltaY));
+
+ // Touch events are a bit more complicated
+ // We need to keep track of the last touch position
+ // and calculate the delta between the current and the last position
+ let lastTouchPosition =-1;
+ window.addEventListener("touchmove",(evt: TouchEvent)=>{
+ const delta = evt.touches[0].clientY - lastTouchPosition;
+ // We only want to apply the delta if it's not TOO big
+ // e.g. when the user is scrolling the page
+ if(delta <10)this.updateTime(-delta);
+ // Update the last touch position
+ lastTouchPosition = evt.touches[0].clientY;
+ });
+ }
+
+ privateupdateTime(delta){
+ if(!this.timeline)return;
+ this.targetTime += delta *0.01*this.scrollSpeed;
+ this.targetTime = Mathf.clamp(this.targetTime,0,this.timeline.duration);
+ }
+
+ onBeforeRender():void{
+ if(!this.timeline)return;
+ this.timeline.pause();
+ this.timeline.time = Mathf.lerp(this.timeline.time,this.targetTime,this.lerpSpeed *this.context.time.deltaTime);
+ this.timeline.evaluate();
+ }
+}
+
+
Code Contribution Example
This is mostly a basic example on how to contribute. It will then be added on our documentation contributions page: https://engine.needle.tools/docs/community/contributions
Please include at least one code snippet, for example like this:
AR Move/Scale/Rotate Controls for Needle on Mobile
This is live for preview over at https://needle-ar.glitch.me/
I will share a github repository if others are interested in collaborating on this, so far I have just sorted functionality for spawning a product model and a floor plane that is large to move it around on. Scale and Rotate work with two finger touches.
I am hoping to figure out how to show and raycast against AR detected planes over here which will remove the need for a large floor plane to raycast against โ Unknown
Public Github Repository: https://github.com/ROBYER1/Needle-AR-Demo
AR Move/Scale/Rotate Controls for Needle on Mobile
This is live for preview over at https://needle-ar.glitch.me/
I will share a github repository if others are interested in collaborating on this, so far I have just sorted functionality for spawning a product model and a floor plane that is large to move it around on. Scale and Rotate work with two finger touches.
I am hoping to figure out how to show and raycast against AR detected planes over here which will remove the need for a large floor plane to raycast against โ Unknown
Public Github Repository: https://github.com/ROBYER1/Needle-AR-Demo
Vertical Move in VR using the right joystick (Quest)
The following code will enable Quest users (haven't tested with other devices) to move up and down with the right-joystick`s y axis. (the x axis being used for snap-turns).
This code will interfere with the teleport script when accidentally pointing towards an object and trying to move up. It is recommended to remove the teleport script for that matter.
The following code enables you to use both controllers in VR (tested on Quest) and scale the player's perspective (XRRig) by squeezing the grab triggers and moving the controllers closer (pinch out) or further apart (pinch in). The boolean allowWorldScaling has to be ticked in unity for that to work.
Upon selecting a draggable object (Drag controls script), the player can scale up or down that object, while keeping the finger on the trigger and squeezing both grab buttons and moving the hands closer or apart.
The current script enables you to visually see the scale. Create a world canvas with a text component as a child. Assign the world canvas to scaleTextObject and the text to scaleText. scaleTextObject will then spawn in front of the player and follow the head movement whenever scaling.
At the moment the position of the hands (controllers) is done by finding the avatar's hands. I couldn't make it work otherwise. If you find a better way please share.
import{ Behaviour, WebXR,serializeable, WebXREvent,WebXRAvatar,GameObject, AvatarMarker,Text}from"@needle-tools/engine";
+import{ Object3D, Vector3,Quaternion,PerspectiveCamera}from"three";
+
+exportclassSqueezeScaleextendsBehaviour{
+
+
+ private webXR?: WebXR;
+
+ private selectedObj: Object3D|null=null;
+
+ @serializeable(Object3D)
+ scaleTextObject: Object3D|null=null;
+
+ @serializeable(Text)
+ scaleText?: Text;
+
+ public allowWorldScaling:boolean=false;
+
+ private leftSqueeze:boolean=false;
+ private rightSqueeze:boolean=false;
+
+ private bothSqueezeStarted=false;
+
+ private rigScaleUpdated=false;
+
+ private initialDistance:number=1;
+ private initialScale:number=1;
+ private newScale:number|null=null;
+
+ private leftHand?:Object3D;
+ private rightHand?:Object3D;
+ private head?:Object3D;
+
+ start():void{
+
+ let _webxr=GameObject.findObjectOfType(WebXR);
+ if(_webxr)
+ {
+ this.webXR=_webxr;
+ console.log("webxr found");
+ }
+
+ //Wait for XR Session
+ WebXR.addEventListener(WebXREvent.XRStarted,()=>{
+ //listen to squeeze events
+ this.context.xrSession?.addEventListener("squeezestart",(event)=>{this.onSqueezeEvent(event,true);});
+ this.context.xrSession?.addEventListener("squeezeend",(event)=>{this.onSqueezeEvent(event,false);});
+ });
+ }
+
+
+ onSqueezeEvent(event: XRInputSourceEvent, status:boolean){
+
+ if(event.inputSource.handedness==="right")
+ {
+ this.rightSqueeze=status;
+ }
+
+ if(event.inputSource.handedness==="left")
+ {
+ this.leftSqueeze=status;
+ }
+ }
+
+ update(){
+
+ if(this.context.isInVR)
+ {
+ //cache object selected if any
+ this.objectGrab();
+
+ //if both grips are squeezed
+ if(this.leftSqueeze &&this.rightSqueeze)
+ {
+ //if object is selected either in the left or right controller (only one)
+ if(this.selectedObj!=null)
+ {
+ //after initial distance value has been set
+ if(this.bothSqueezeStarted)
+ {
+ //get current distance between controllers
+ const scaleValue=this.calculateDistance();
+
+ //get distance change since beginning of squeeze to get a "pinch in/out" effect
+ this.newScale=this.initialScale+scaleValue-this.initialDistance;
+
+ //avoid 0 and negative scales
+ if(this.newScale<0.001){this.newScale=0.001;}
+
+ // scale object according to new distance since initial distance
+ this.selectedObj.scale.x=this.newScale;
+ this.selectedObj.scale.y=this.newScale;
+ this.selectedObj.scale.z=this.newScale;
+
+ this.showVisual(this.newScale,"Object :");
+ }
+ else
+ {
+ //get initial distance value (only once at a new squeeze both hands event)
+ this.bothSqueezeStarted=true;
+
+ this.initialDistance=this.calculateDistance();
+
+ //cache object's initial scale
+ this.initialScale=this.selectedObj.scale.x;
+ }
+ }
+ else
+ {
+ //scale world ?
+ if(this.webXR?.Rig &&this.allowWorldScaling)
+ {
+ //after initial distance value has been set
+ if(this.bothSqueezeStarted)
+ {
+ //get current distance between controllers
+ const scaleValue=this.calculateDistance();
+
+ //get distance change since beginning of squeeze to get a "pinch in/out" effect
+ this.newScale=this.initialScale+scaleValue-this.initialDistance;
+
+ //avoid 0 and negative scales
+ if(this.newScale<0.001){this.newScale=0.001;}
+
+ this.showVisual(this.newScale,"World :");
+
+ this.rigScaleUpdated=true;
+ }
+ else
+ {
+ //get initial distance value (only once at a new squeeze both hands event)
+ this.bothSqueezeStarted=true;
+
+ this.initialDistance=this.calculateDistance();
+
+ //cache object's initial scale
+ this.initialScale=this.webXR.Rig.scale.x;
+ }
+ }
+ }
+
+ }
+ else
+ {
+ //reset values
+ this.bothSqueezeStarted=false;
+
+ //if world has been scaled, scale rig accordingly at the end of squeezing and once only
+ if(this.webXR?.Rig &&this.rigScaleUpdated &&this.newScale)
+ {
+ //change rig scale
+ this.webXR.Rig.scale.set(this.newScale,this.newScale,this.newScale);
+ this.webXR.Rig.updateMatrixWorld();
+
+ const cam =this.context.mainCamera as PerspectiveCamera;
+ cam.near=this.newScale>2?0.0001:0.2;
+ cam.updateProjectionMatrix();
+
+ //reset
+ this.rigScaleUpdated=false;
+ }
+
+ if(this.scaleTextObject)
+ {
+ this.scaleTextObject.visible=false;
+ }
+
+ this.newScale=null;
+ }
+ }
+ }
+
+ privatecalculateDistance():number
+ {
+ let distance=1;
+
+ if(this.leftHand &&this.rightHand)
+ {
+
+ const left=this.leftHand.position;
+ const right=this.rightHand.position;
+
+ // Calculate the difference between the positions
+ const dx = left.x - right.x;
+ const dy = left.y - right.y;
+ const dz = left.z - right.z;
+
+ // Calculate the distance using the Euclidean distance formula
+ distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
+ }
+ else
+ {
+ //set positions of controllers from your avatar (only once)
+ let allAvatars = AvatarMarker.instances;
+ if(allAvatars.length>0)
+ {
+ for(let i=0;i<allAvatars.length;i++)
+ {
+ if(allAvatars[i].isLocalAvatar())
+ {
+ const av=allAvatars[i].avatar as WebXRAvatar;
+ if(av!=null)
+ {
+ this.leftHand=av.handLeft as Object3D;
+ this.rightHand=av.handRight as Object3D;
+ this.head = av.head as Object3D;
+ }
+ }
+ }
+ }
+ }
+
+ return distance;
+ }
+
+ showVisual(scale:number, mesg:string):void
+ {
+ if(this.scaleTextObject &&this.head &&this.scaleText)
+ {
+ this.scaleTextObject.visible=true;
+
+ const offset =newVector3(0,0,7);
+ offset.applyQuaternion(this.head.quaternion);
+
+ this.scaleTextObject.position.copy(this.head.position.add(offset));
+
+ const roundedNum=+scale.toFixed(3);
+ this.scaleText.text=mesg+" "+roundedNum;
+ }
+
+ }
+
+
+ objectGrab():void
+ {
+ if(this.webXR?.RightController?.grabbed?.selected)
+ {
+ this.selectedObj =this.webXR.RightController.grabbed.selected;
+ }
+ elseif(this.webXR?.LeftController?.grabbed?.selected)
+ {
+ this.selectedObj =this.webXR.LeftController.grabbed.selected;
+ }
+ else
+ {
+ this.selectedObj=null;
+ }
+ }
+}
+
Network instantiation of multiple objects
In a multiuser session, typically objects are instantiated using instantiateSynced as such:
My particular use-case was for generating programmatically a random scene made of cubes, and that scene had to be the same for all users of the same room. I had used the example above but for some unknown reasons sometimes the scenes were partially rendered when instantiating simultaneously >400 objects. @Marcel of Needle suggested to generate a seed (position of all objects in the scene) and send that seed instead using :
this.context.connection.send()
+
All users using :
this.context.connection.beginListen()
+
would receive any seed previously sent, upon joining the same room, allowing them to instantiate cubes according to that seed (array of Vector3).
Here is a script illustrating the use of the send method and the beginListen counterpart:
+//This is an example of sending the seed of a randomly generated scene made of cubes, for all other instances logging into the same room to create the same scene.
+
+//This script requires a prefab (e.g. a 1x1x1 Cube)
+//This script will generate and build randomly positioned cubes (random walk) as a child of the object it is attached to.
+//The generateSeed() method is in this script called via a button. The button is deactivated once the seed has been transmitted.
+//Any users joining the same room will receive the seed and build the exact same scene
+
+
+import{ Behaviour,GameObject,serializable,InstantiateOptions}from"@needle-tools/engine";
+import{ Vector3, Object3D }from"three";
+
+
+exportclassNetworkedSeedextendsBehaviour
+{
+ @serializable(Object3D)
+ prefab?: GameObject;
+
+ @serializable(Object3D)
+ generateButton?: Object3D;
+
+ public seedSize:number=30;
+
+ seed: Vector3[]=[];
+
+ onEnable():void{
+ this.context.connection.beginListen("mySeed",this.onDataReceived);
+ if(this.generateButton)
+ {
+ this.generateButton.visible=true;
+ }
+ }
+ onDisable():void{
+ this.context.connection.stopListen("mySeed",this.onDataReceived);
+ }
+
+ onDataReceived=(data:any)=>{
+
+ console.log("Received data:", data.mySeed);
+ if(this.seed.length===0)
+ {
+ //prevent other generations of the seed
+ if(this.generateButton)
+ {
+ this.generateButton.visible=false;
+ }
+
+ this.seed=data.mySeed;
+ //build scene
+ this.buildScene();
+ }
+ };
+
+
+ //generate and send seed to all from the button generateButton
+ publicgenerateSeed():void{
+
+ if(this.seed.length==0)//no seed found => generate one
+ {
+ this.seed =[];
+ const uniquePositions =newSet<string>();
+
+ //start at origin
+ const startPosition =newVector3(0,0,0);
+ this.seed.push(startPosition.clone());
+ uniquePositions.add(startPosition.toArray().toString());
+
+ //go for a random walk of length : seedSize
+ while(this.seed.length <this.seedSize){
+ const lastPosition =this.seed[this.seed.length -1];
+ let newPosition: Vector3;
+
+ //walk and add position, making sure they are unique
+ do{
+ const direction =this.getRandomDirection();
+ newPosition = lastPosition.clone().add(direction);
+ }while(uniquePositions.has(newPosition.toArray().toString()));
+
+ this.seed.push(newPosition.clone());
+ uniquePositions.add(newPosition.toArray().toString());
+ }
+
+ //send the seed to all on the server
+ this.sendSeed();
+
+ //prevent other generations of the seed
+ if(this.generateButton)
+ {
+ this.generateButton.visible=false;
+ }
+ }
+
+ //build scene locally
+ this.buildScene();
+ }
+
+ privatesendSeed():void{
+ if(this.seed.length!=0)
+ {
+ this.context.connection.send("mySeed",{guid:this.guid, mySeed:this.seed});
+ console.log("------ SEED SENT -------");
+ }
+ }
+
+ publicbuildScene():void{
+
+ //check if the seed is not empty
+ if(this.seed.length==0)
+ {
+ console.log("array was empty");
+ return;
+ }
+
+ //check if the scene has already been built
+ if(this.gameObject.children.length>0)
+ {
+ console.log("Scene already present");
+ return;
+ }
+
+ // Create cubes at each position of the random walk
+ for(let i=0; i<this.seed.length; i++)
+ {
+ const option =newInstantiateOptions();
+ option.context =this.context;
+ option.parent=this.gameObject;
+ option.position =this.seed[i];
+
+ if(this.prefab!=null)
+ {
+ const cube = GameObject.instantiate(this.prefab, option)as GameObject;
+ }
+ }
+
+ console.log("----------- Scene Built ---------");
+
+ }
+
+ privategetRandomDirection(): Vector3 {
+ const x = Math.random()<0.5?-1:1;
+ const y = Math.random()<0.5?-1:1;
+ const z = Math.random()<0.5?-1:1;
+ returnnewVector3(x, y, z);
+ }
+
+}
+
The above script is placed on an object (any Transform) and will generate an array of unique Vector3 positions for a specified length (seedSize) after generateSeed() is called (In this case it is called from a button: generateButton).
Once generated it will send the array to the server and build the scene. The building process consist of instantiating the prefab at each Vector3 position of the seed (this.seed) array.
Any user joining the same room after a seed has been generated and sent, will receive the seed from the server and trigger the callback onDataReceived() which will cache the seed array, disable the button, and build the scene with the prefab, according to the seed.
This gives a way to generate a scene and communicate the seed of that scene, for each user to build locally.
This was the solution I chose which worked better than instantiating a complex scene (>400 objects) with instantiateSynced which would occasionally cause bugs.
My particular use-case was for generating programmatically a random scene made of cubes, and that scene had to be the same for all users of the same room. I had used the example above but for some unknown reasons sometimes the scenes were partially rendered when instantiating simultaneously >400 objects. @Marcel of Needle suggested to generate a seed (position of all objects in the scene) and send that seed instead using :
this.context.connection.send()
+
All users using :
this.context.connection.beginListen()
+
would receive any seed previously sent, upon joining the same room, allowing them to instantiate cubes according to that seed (array of Vector3).
Here is a script illustrating the use of the send method and the beginListen counterpart:
+//This is an example of sending the seed of a randomly generated scene made of cubes, for all other instances logging into the same room to create the same scene.
+
+//This script requires a prefab (e.g. a 1x1x1 Cube)
+//This script will generate and build randomly positioned cubes (random walk) as a child of the object it is attached to.
+//The generateSeed() method is in this script called via a button. The button is deactivated once the seed has been transmitted.
+//Any users joining the same room will receive the seed and build the exact same scene
+
+
+import{ Behaviour,GameObject,serializable,InstantiateOptions}from"@needle-tools/engine";
+import{ Vector3, Object3D }from"three";
+
+
+exportclassNetworkedSeedextendsBehaviour
+{
+ @serializable(Object3D)
+ prefab?: GameObject;
+
+ @serializable(Object3D)
+ generateButton?: Object3D;
+
+ public seedSize:number=30;
+
+ seed: Vector3[]=[];
+
+ onEnable():void{
+ this.context.connection.beginListen("mySeed",this.onDataReceived);
+ if(this.generateButton)
+ {
+ this.generateButton.visible=true;
+ }
+ }
+ onDisable():void{
+ this.context.connection.stopListen("mySeed",this.onDataReceived);
+ }
+
+ onDataReceived=(data:any)=>{
+
+ console.log("Received data:", data.mySeed);
+ if(this.seed.length===0)
+ {
+ //prevent other generations of the seed
+ if(this.generateButton)
+ {
+ this.generateButton.visible=false;
+ }
+
+ this.seed=data.mySeed;
+ //build scene
+ this.buildScene();
+ }
+ };
+
+
+ //generate and send seed to all from the button generateButton
+ publicgenerateSeed():void{
+
+ if(this.seed.length==0)//no seed found => generate one
+ {
+ this.seed =[];
+ const uniquePositions =newSet<string>();
+
+ //start at origin
+ const startPosition =newVector3(0,0,0);
+ this.seed.push(startPosition.clone());
+ uniquePositions.add(startPosition.toArray().toString());
+
+ //go for a random walk of length : seedSize
+ while(this.seed.length <this.seedSize){
+ const lastPosition =this.seed[this.seed.length -1];
+ let newPosition: Vector3;
+
+ //walk and add position, making sure they are unique
+ do{
+ const direction =this.getRandomDirection();
+ newPosition = lastPosition.clone().add(direction);
+ }while(uniquePositions.has(newPosition.toArray().toString()));
+
+ this.seed.push(newPosition.clone());
+ uniquePositions.add(newPosition.toArray().toString());
+ }
+
+ //send the seed to all on the server
+ this.sendSeed();
+
+ //prevent other generations of the seed
+ if(this.generateButton)
+ {
+ this.generateButton.visible=false;
+ }
+ }
+
+ //build scene locally
+ this.buildScene();
+ }
+
+ privatesendSeed():void{
+ if(this.seed.length!=0)
+ {
+ this.context.connection.send("mySeed",{guid:this.guid, mySeed:this.seed});
+ console.log("------ SEED SENT -------");
+ }
+ }
+
+ publicbuildScene():void{
+
+ //check if the seed is not empty
+ if(this.seed.length==0)
+ {
+ console.log("array was empty");
+ return;
+ }
+
+ //check if the scene has already been built
+ if(this.gameObject.children.length>0)
+ {
+ console.log("Scene already present");
+ return;
+ }
+
+ // Create cubes at each position of the random walk
+ for(let i=0; i<this.seed.length; i++)
+ {
+ const option =newInstantiateOptions();
+ option.context =this.context;
+ option.parent=this.gameObject;
+ option.position =this.seed[i];
+
+ if(this.prefab!=null)
+ {
+ const cube = GameObject.instantiate(this.prefab, option)as GameObject;
+ }
+ }
+
+ console.log("----------- Scene Built ---------");
+
+ }
+
+ privategetRandomDirection(): Vector3 {
+ const x = Math.random()<0.5?-1:1;
+ const y = Math.random()<0.5?-1:1;
+ const z = Math.random()<0.5?-1:1;
+ returnnewVector3(x, y, z);
+ }
+
+}
+
The above script is placed on an object (any Transform) and will generate an array of unique Vector3 positions for a specified length (seedSize) after generateSeed() is called (In this case it is called from a button: generateButton).
Once generated it will send the array to the server and build the scene. The building process consist of instantiating the prefab at each Vector3 position of the seed (this.seed) array.
Any user joining the same room after a seed has been generated and sent, will receive the seed from the server and trigger the callback onDataReceived() which will cache the seed array, disable the button, and build the scene with the prefab, according to the seed.
This gives a way to generate a scene and communicate the seed of that scene, for each user to build locally.
This was the solution I chose which worked better than instantiating a complex scene (>400 objects) with instantiateSynced which would occasionally cause bugs.
The following code enables you to use both controllers in VR (tested on Quest) and scale the player's perspective (XRRig) by squeezing the grab triggers and moving the controllers closer (pinch out) or further apart (pinch in). The boolean allowWorldScaling has to be ticked in unity for that to work.
Upon selecting a draggable object (Drag controls script), the player can scale up or down that object, while keeping the finger on the trigger and squeezing both grab buttons and moving the hands closer or apart.
The current script enables you to visually see the scale. Create a world canvas with a text component as a child. Assign the world canvas to scaleTextObject and the text to scaleText. scaleTextObject will then spawn in front of the player and follow the head movement whenever scaling.
At the moment the position of the hands (controllers) is done by finding the avatar's hands. I couldn't make it work otherwise. If you find a better way please share.
import{ Behaviour, WebXR,serializeable, WebXREvent,WebXRAvatar,GameObject, AvatarMarker,Text}from"@needle-tools/engine";
+import{ Object3D, Vector3,Quaternion,PerspectiveCamera}from"three";
+
+exportclassSqueezeScaleextendsBehaviour{
+
+
+ private webXR?: WebXR;
+
+ private selectedObj: Object3D|null=null;
+
+ @serializeable(Object3D)
+ scaleTextObject: Object3D|null=null;
+
+ @serializeable(Text)
+ scaleText?: Text;
+
+ public allowWorldScaling:boolean=false;
+
+ private leftSqueeze:boolean=false;
+ private rightSqueeze:boolean=false;
+
+ private bothSqueezeStarted=false;
+
+ private rigScaleUpdated=false;
+
+ private initialDistance:number=1;
+ private initialScale:number=1;
+ private newScale:number|null=null;
+
+ private leftHand?:Object3D;
+ private rightHand?:Object3D;
+ private head?:Object3D;
+
+ start():void{
+
+ let _webxr=GameObject.findObjectOfType(WebXR);
+ if(_webxr)
+ {
+ this.webXR=_webxr;
+ console.log("webxr found");
+ }
+
+ //Wait for XR Session
+ WebXR.addEventListener(WebXREvent.XRStarted,()=>{
+ //listen to squeeze events
+ this.context.xrSession?.addEventListener("squeezestart",(event)=>{this.onSqueezeEvent(event,true);});
+ this.context.xrSession?.addEventListener("squeezeend",(event)=>{this.onSqueezeEvent(event,false);});
+ });
+ }
+
+
+ onSqueezeEvent(event: XRInputSourceEvent, status:boolean){
+
+ if(event.inputSource.handedness==="right")
+ {
+ this.rightSqueeze=status;
+ }
+
+ if(event.inputSource.handedness==="left")
+ {
+ this.leftSqueeze=status;
+ }
+ }
+
+ update(){
+
+ if(this.context.isInVR)
+ {
+ //cache object selected if any
+ this.objectGrab();
+
+ //if both grips are squeezed
+ if(this.leftSqueeze &&this.rightSqueeze)
+ {
+ //if object is selected either in the left or right controller (only one)
+ if(this.selectedObj!=null)
+ {
+ //after initial distance value has been set
+ if(this.bothSqueezeStarted)
+ {
+ //get current distance between controllers
+ const scaleValue=this.calculateDistance();
+
+ //get distance change since beginning of squeeze to get a "pinch in/out" effect
+ this.newScale=this.initialScale+scaleValue-this.initialDistance;
+
+ //avoid 0 and negative scales
+ if(this.newScale<0.001){this.newScale=0.001;}
+
+ // scale object according to new distance since initial distance
+ this.selectedObj.scale.x=this.newScale;
+ this.selectedObj.scale.y=this.newScale;
+ this.selectedObj.scale.z=this.newScale;
+
+ this.showVisual(this.newScale,"Object :");
+ }
+ else
+ {
+ //get initial distance value (only once at a new squeeze both hands event)
+ this.bothSqueezeStarted=true;
+
+ this.initialDistance=this.calculateDistance();
+
+ //cache object's initial scale
+ this.initialScale=this.selectedObj.scale.x;
+ }
+ }
+ else
+ {
+ //scale world ?
+ if(this.webXR?.Rig &&this.allowWorldScaling)
+ {
+ //after initial distance value has been set
+ if(this.bothSqueezeStarted)
+ {
+ //get current distance between controllers
+ const scaleValue=this.calculateDistance();
+
+ //get distance change since beginning of squeeze to get a "pinch in/out" effect
+ this.newScale=this.initialScale+scaleValue-this.initialDistance;
+
+ //avoid 0 and negative scales
+ if(this.newScale<0.001){this.newScale=0.001;}
+
+ this.showVisual(this.newScale,"World :");
+
+ this.rigScaleUpdated=true;
+ }
+ else
+ {
+ //get initial distance value (only once at a new squeeze both hands event)
+ this.bothSqueezeStarted=true;
+
+ this.initialDistance=this.calculateDistance();
+
+ //cache object's initial scale
+ this.initialScale=this.webXR.Rig.scale.x;
+ }
+ }
+ }
+
+ }
+ else
+ {
+ //reset values
+ this.bothSqueezeStarted=false;
+
+ //if world has been scaled, scale rig accordingly at the end of squeezing and once only
+ if(this.webXR?.Rig &&this.rigScaleUpdated &&this.newScale)
+ {
+ //change rig scale
+ this.webXR.Rig.scale.set(this.newScale,this.newScale,this.newScale);
+ this.webXR.Rig.updateMatrixWorld();
+
+ const cam =this.context.mainCamera as PerspectiveCamera;
+ cam.near=this.newScale>2?0.0001:0.2;
+ cam.updateProjectionMatrix();
+
+ //reset
+ this.rigScaleUpdated=false;
+ }
+
+ if(this.scaleTextObject)
+ {
+ this.scaleTextObject.visible=false;
+ }
+
+ this.newScale=null;
+ }
+ }
+ }
+
+ privatecalculateDistance():number
+ {
+ let distance=1;
+
+ if(this.leftHand &&this.rightHand)
+ {
+
+ const left=this.leftHand.position;
+ const right=this.rightHand.position;
+
+ // Calculate the difference between the positions
+ const dx = left.x - right.x;
+ const dy = left.y - right.y;
+ const dz = left.z - right.z;
+
+ // Calculate the distance using the Euclidean distance formula
+ distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
+ }
+ else
+ {
+ //set positions of controllers from your avatar (only once)
+ let allAvatars = AvatarMarker.instances;
+ if(allAvatars.length>0)
+ {
+ for(let i=0;i<allAvatars.length;i++)
+ {
+ if(allAvatars[i].isLocalAvatar())
+ {
+ const av=allAvatars[i].avatar as WebXRAvatar;
+ if(av!=null)
+ {
+ this.leftHand=av.handLeft as Object3D;
+ this.rightHand=av.handRight as Object3D;
+ this.head = av.head as Object3D;
+ }
+ }
+ }
+ }
+ }
+
+ return distance;
+ }
+
+ showVisual(scale:number, mesg:string):void
+ {
+ if(this.scaleTextObject &&this.head &&this.scaleText)
+ {
+ this.scaleTextObject.visible=true;
+
+ const offset =newVector3(0,0,7);
+ offset.applyQuaternion(this.head.quaternion);
+
+ this.scaleTextObject.position.copy(this.head.position.add(offset));
+
+ const roundedNum=+scale.toFixed(3);
+ this.scaleText.text=mesg+" "+roundedNum;
+ }
+
+ }
+
+
+ objectGrab():void
+ {
+ if(this.webXR?.RightController?.grabbed?.selected)
+ {
+ this.selectedObj =this.webXR.RightController.grabbed.selected;
+ }
+ elseif(this.webXR?.LeftController?.grabbed?.selected)
+ {
+ this.selectedObj =this.webXR.LeftController.grabbed.selected;
+ }
+ else
+ {
+ this.selectedObj=null;
+ }
+ }
+}
+
Vertical Move in VR using the right joystick (Quest)
The following code will enable Quest users (haven't tested with other devices) to move up and down with the right-joystick`s y axis. (the x axis being used for snap-turns).
This code will interfere with the teleport script when accidentally pointing towards an object and trying to move up. It is recommended to remove the teleport script for that matter.
When working in Unity or Blender then you will notice that when you create a new Needle Engine component in Typescript or Javascript it will automatically generate a Unity C# stub component OR a Blender panel for you.
This is thanks to the magic of the Needle component compileropen in new window that runs behind the scenes in an editor environment and watches changes to your script files. When it notices that you created a new Needle Engine component it will then generate the correct Unity component or Blender panel including public variables or properties that you can then set or link from within the Editor.
If you want to add scripts inside the src/scripts folder in your project then you need to have a Component Generator on the GameObject with your ExportInfo component. Now when adding new components in your/threejs/project/src/scriptsit will automatically generate Unity scripts in Assets/Needle/Components.codegen. If you want to add scripts to any NpmDef file you can just create them - each NpmDef automatically watches script changes and handles component generation, so you don't need any additional component in your scene.
For C# fields to be correctly generated it is currently important that you explictly declare a Typescript type. For example myField : number = 5
You can switch between Typescript input and generated C# stub components using the tabs below
Component C# classes are generated with the partialopen in new window flag so that it is easy to extend them with functionality. This is helpful to draw gizmos, add context menus or add additional fields or methods that are not part of a built-in component.
Member Casing
Exported members will start with a lowercase letter. For example if your C# member is named MyString it will be assigned to myString.
Here is a overview of some of the components that we provide. Some of them map directly to Unity components, while others are core components from Needle Engine.
For a complete list please have a look at the components inside the folders node_modules/@needle-tools/engine/engine-components and engine-components-experimental.
You can always add your own components or add wrappers for Unity components we haven't provided yet.
DirectionalLight, PointLight, Spotlight. Note that you can use it to bake light (e.g. Rectangular Light shapes) as well
XRFlag
Control when objects will be visible. E.g. only enable object when in AR
DeviceFlag
Control on which device objects will be visible
LODGroup
ParticleSystem
Experimental and currently not fully supported
VideoPlayer
Playback videos from url or referenced video file (will be copied to output on export). The VideoPlayer also supports streaming from MediaStream objects or M3U8 livestream URLs
MeshRenderer
Used to handle rendering of objects including lightmapping and instancing
Postprocessing effects use the pmndrs postprocessing libraryopen in new window under the hood. This means you can also easily add your own custom effects and get an automatically optimized postprocessing pass.
Unity only: Note that Postprocessing effect export in Unity is only supported with URP.
Unity only: extra Component means that the effect component is an extra component that has to be added next to the Volume component. For example for Antialiasing add a Volume component and an Antialiasing component to the same GameObject.
Effect Name
Antialiasing
extra Unity Component
Bloom
via Volume asset
Chromatic Aberration
via Volume asset
Color Adjustments / Color Correction
via Volume asset
Depth Of Field
via Volume asset
Pixelation
Screenspace Ambient Occlusion N8
Screenspace Ambient Occlusion
Tilt Shift Effect
Vignette
via Volume asset
Your custom effect
Create a new class that extends from Needle Engine's PostProcessingEffect class. Then call registerCustomEffectType with your effect name and class type.
Spatial UI components are mapped from Unity UI (Canvas, not UI Toolkit) to three-mesh-uiopen in new window. UI can be animated.
Name
Description
Canvas
Unity's UI system. Needs to be in World Space mode right now.
Text
Render Text using Unity's UI. Custom fonts are supported, a font atlas will be automatically generated on export. Use the font settings to control which characters are included in the atlas
Button
Receives click events - use the onClick event to react to it. It can be added too 3D scene objects as well
Image
Renders a sprite image
RawImage
Renders a texture
Note: Depending on your project, often a mix of spatial and 2D UI makes sense for cross-platform projects where VR, AR, and screens are supported. Typically, you'd build the 2D parts with HTML for best accessibility, and the 3D parts with geometric UIs that also support depth offsets (e.g. button hover states and the like).
Handles loading and unloading of other scenes or prefabs / glTF files. Has features to preload, change scenes via swiping, keyboard events or URL navigation
Needle Engine also has some very powerful and useful debugging methods that are part of the static Gizmos class. See the scripting documentation for more information.
You can attach VSCode to the running local server to set breakpoints and debug your code. You can read more about debugging with VSCodeopen in new window here.
Create a launch.json file at .vscode/launch.json in your web project with the following content:
If you have changed the port on which your server starts make sure to update the url field accordingly. You can then start your local server from within VSCode:
For Android debugging, you can attach Chrome Dev Tools to your device and see logs right from your PC. You have to switch your device into development mode and connect it via USB.
Deployment is the process of making your application available to the public on a website. Needle Engine ensures that your project is as small and fast as possible by using the latest compression techniques such as KTX2, Draco, and Meshopt.
Open File/Build Settings and select Needle Engine for options!
Where do I find the Build Options in Unity?
To build your web project for deployment to a web server you can click Build in the Unity Editor Build Settings Window. You can enable the Development Build checkbox to omit compression (see below) which requires toktx to be installed on your machine.
Nodejs is only required during development. The distributed website (using our default vite template) is a static page that doesn't rely on Nodejs and can be put on any regular web server. Nodejs is required if you want to run our minimalistic networking server on the same web server (automatically contained in the Glitch deployment process).
See guides above on how to access the options from within your Editor (e.g. Unity or Blender).
The main difference to a production build is that it does not perform ktx2open in new window and dracoopen in new window compression (for reduction of file size and loading speed) as well as the option to progressively load high-quality textures.
We generally recommend making production builds for optimized file size and loading speed (see more information below).
To make a production build, you need to have toktxopen in new window installed, which provides texture compression using the KTX2 supercompression format. Please go to the toktx Releases Pageopen in new window and download and install the latest version (v4.1.0 at the time of writing). You may need to restart Unity after installing it. If you're sure that you have installed toktx and it's part of your PATH but still can't be found, please restart your machine and try build again.
Production builds will by default compress textures using KTX2 (either ETC1S or UASTC depending on their usage in the project) but you can also select WebP compression and select a quality level.
# How do I choose between ETC1S, UASTC and WebP compression?
Format
ETC1S
UASTC
WebP
GPU Memory Usage
Low
Low
High (uncompressed)
File Size
Low
High
Very low
Quality
Medium
Very high
Depends on quality setting
Typical usage
Works for everything, but best for color textures
High-detail data textures: normal maps, roughness, metallic, etc.
Files where ETC1S quality is not sufficient but UASTC is too large
You have the option to select texture compression and progressive loading options per Texture by using the Needle Texture Importer in Unity or in the Material tab in Blender.
Unity: How can I set per-texture compression settings?
Blender: How can I set per-texture compression settings?
Select the material tab. You will see compression options for all textures that are being used by that material.
Toktx can not be found
Windows: Make sure you have added toktx to your system environment variables. You may need to restart your computer after adding it to refresh the environment variables. The default install location is C:\Program Files\KTX-Software\bin
You can also add the Progressive Texture Settings component anywhere in your scene, to make all textures in your project be progressively loaded. Progressive loading is not applied to lightmaps or skybox textures at this point.
With progressive loading textures will first be loaded using a lower resolution version. A full quality version will be loaded dynamically when the texture becomes visible. This usually reduces initial loading of your scene significantly.
How can I enable progressive texture loading?
# Progressive textures can be enabled per texture or for all textures in your project:
# Enable for all textures in the project that don't have any other specific setting:
By default, a production build will compress meshes using Draco compression. Use the MeshCompression component to select between draco and mesh-opt per exported glTF. Additionally you can setup mesh simplification to reduce the polycount for production builds in the mesh import settings (Unity). When viewing your application in the browser, you can append ?wireframe to your URL to preview the meshes.
Glitchopen in new window provides a fast and free way for everyone to host small and large websites. We're providing an easy way to remix and deploy to a new Glitch page (based on our starter), and also to run a minimalistic networking server on the same Glitch page if needed.
You can deploy to glitch by adding the DeployToGlitch component to your scene and following the instructions.
Note that free projects hosted on glitch may not exceed ~100 MB. If you need to upload a larger project consider using a different deployment target.
How do I deploy to Glitch from Unity?
Add the DeployToGlitch component to the GameObject that also has the ExportInfo component.
Click the Create new Glitch Remix button on the component
Glitch will now create a remix of the template. Copy the URL from your browser
Open Unity again and paste the URL in the Project Name field of your Deploy To Glitch component
Wait a few seconds until Unity has received your deployment key from glitch (this key is safely stored in the .env file on glitch. Do not share it with others, everyone with this key will be able to upload to your glitch website)
Once the Deploy Key has been received you can click the Build & Deploy button to upload to glitch.
How do I deploy to Glitch from Blender?
Find the Deploy To Glitch panel in the Scene tab
Click the Remix on glitch button on the component
Your browser will open the glitch project template
Wait for Glitch to generate a new project
Copy paste the project URL in the Blender DeployToGlitch panel as the project name (you can paste the full URL, the panel will extract the necessary information)
On Glitch open the .env file and enter a password in the field Variable Value next to the DEPLOY_KEY
Enter the same password in Blender in the Key field
Click the DeployToGlitch button to build and upload your project to glitch. A browser will open when the upload has finished. Try to refresh the page if it shows black after having opened it.
If you click Create new Glitch Remix and the browser shows an error like there was an error starting the editor you can click OK. Then go to glitch.comopen in new window and make sure you are signed in. After that you then try clicking the button again in Unity or Blender.
Just add the DeployToNetlify component to your scene and follow the instructions. You can create new projects with the click of a button or deployt to existing projects.
If you see this error after uploading your project make sure you do not upload a gzipped index.html. You can disable gzip compression in vite.config.js in your Needle web project folder. Just remove the line with viteCompression({ deleteOriginFile: true }). The build your project again and upload to itch.
Add the DeployToFTP componentยน on a GameObject in your scene (it is good practice to add it to the same GameObject as ExportInfo - but it is not mandatory)
Assign an FTP server asset and fill out server, username, and password if you have not already ยฒ This asset contains the access information to your FTP server - you get them when you create a new FTP account at your hosting provider
Click the Build & Deploy button on the DeployToFTP component to build your project and uploading it to your FTP account
ยน Deploy to FTP component
ยฒ FTP Server asset containing the access information of your FTP user account
Deploy To FTP component after server asset is assigned. You can directly deploy to a subfolder on your server using the path field
How do I deploy to my FTP server manually?
Open File > Build Settings, select Needle Engine, and click on Build
Wait for the build to complete - the resulting dist folder will open automatically after all build and compression steps have run.
Copy the files from the dist folder to your FTP storage.
That's it! ๐
Note: If the result doesn't work when uploaded it might be that your web server does not support serving gzipped files. You have two options to fix the problem: Option 1: You can try enabling gzip compression on your server using a htaccess file! Option 2: You can turn gzip compression off in the build settings at File/Build Window and selecting the Needle Engine platform.
Note: If you're getting errors during compression, please let us know and report a bug! If your project works locally and only fails when doing production builds, you can get unstuck right away by doing a Development Build. For that, simply toggle Development Build on in the Build Settings.
With Needle Engine you can build to Facebook Instant Games automatically No manual adjustments to your web app or game are required.
How do I deploy to Facebook Instant Games from Unity?
Add the Deploy To Facebook Instant Games component to your scene:
Click the Build For Instant Games button
After the build has finished you will get a ZIP file that you can upload to your facebook app.
On Facebook add the Instant Games module and go to Instant Games/Web hosting
You can upload your zip using the Upload version button (1). After the upload has finished and the zip has been processed click the Stage for testing button to test your app (2, here the blue button) or Push to production (the button with the star icon)
That's it - you can then click the Play button next to each version to test your game on facebook.
How do I create a app on Facebook (with Instant Games capabilities)
After creating the app add the Instant Games product
Here you can find the official instant games documentationopen in new window on facebook. Note that all you have to do is to create an app with instant games capabilities. We will take care of everything else and no manual adjustments to your Needle Engine website are required.
It's possible to create regular Unity projects where you can build both to Needle Engine and to regular Unity platforms such as Desktop or even WebGL. Our "component mapping" approach means that no runtime logic is modified inside Unity - if you want you can regularily use Play Mode and build to other target platforms. In some cases this will mean that you have duplicate code (C# code and matching TypeScript logic). The amount of extra work through this depends on your project.
Enter Play Mode in Unity In Project Settings > Needle Engine, you can turn off Override Play Mode and Override Build settings to switch between Needle's build process and Unity's build process:
Needle Engine for Unity supports various commandline arguments to export single assets (Prefabs or Scenes) or to build a whole web project in batch mode (windowsless).
The following list gives a table over the available options:
-scene
path to a scene or a asset to be exported e.g. Assets/path/to/myObject.prefab or Assets/path/to/myScene.unity
-outputPath <path/to/output.glb>
set the output path for the build (only valid when building a scene)
-buildProduction
run a production build
-buildDevelopment
run a development build
-debug
open a console window for debugging
+
+
+
diff --git a/deployment/buildoptions_gzip.jpg b/deployment/buildoptions_gzip.jpg
new file mode 100644
index 000000000..0472b9701
Binary files /dev/null and b/deployment/buildoptions_gzip.jpg differ
diff --git a/deployment/deploytofacebookinstantgames-hosting.jpg b/deployment/deploytofacebookinstantgames-hosting.jpg
new file mode 100644
index 000000000..63ef163c7
Binary files /dev/null and b/deployment/deploytofacebookinstantgames-hosting.jpg differ
diff --git a/deployment/deploytofacebookinstantgames-upload.jpg b/deployment/deploytofacebookinstantgames-upload.jpg
new file mode 100644
index 000000000..826fd6e5c
Binary files /dev/null and b/deployment/deploytofacebookinstantgames-upload.jpg differ
diff --git a/deployment/deploytofacebookinstantgames.jpg b/deployment/deploytofacebookinstantgames.jpg
new file mode 100644
index 000000000..d0dfb4107
Binary files /dev/null and b/deployment/deploytofacebookinstantgames.jpg differ
diff --git a/deployment/deploytoftp.jpg b/deployment/deploytoftp.jpg
new file mode 100644
index 000000000..85ae4b87f
Binary files /dev/null and b/deployment/deploytoftp.jpg differ
diff --git a/deployment/deploytoftp2.jpg b/deployment/deploytoftp2.jpg
new file mode 100644
index 000000000..16d1173de
Binary files /dev/null and b/deployment/deploytoftp2.jpg differ
diff --git a/deployment/deploytoftp3.jpg b/deployment/deploytoftp3.jpg
new file mode 100644
index 000000000..cb7a1f242
Binary files /dev/null and b/deployment/deploytoftp3.jpg differ
diff --git a/deployment/deploytogithubpages.jpg b/deployment/deploytogithubpages.jpg
new file mode 100644
index 000000000..19978c16b
Binary files /dev/null and b/deployment/deploytogithubpages.jpg differ
diff --git a/deployment/deploytoglitch-1.jpg b/deployment/deploytoglitch-1.jpg
new file mode 100644
index 000000000..c45bcd0ee
Binary files /dev/null and b/deployment/deploytoglitch-1.jpg differ
diff --git a/deployment/deploytoglitch-2.jpg b/deployment/deploytoglitch-2.jpg
new file mode 100644
index 000000000..35c3e40d6
Binary files /dev/null and b/deployment/deploytoglitch-2.jpg differ
diff --git a/deployment/deploytonetlify-2.jpg b/deployment/deploytonetlify-2.jpg
new file mode 100644
index 000000000..30f80f9ab
Binary files /dev/null and b/deployment/deploytonetlify-2.jpg differ
diff --git a/deployment/deploytonetlify.jpg b/deployment/deploytonetlify.jpg
new file mode 100644
index 000000000..e56f2ed5c
Binary files /dev/null and b/deployment/deploytonetlify.jpg differ
diff --git a/deployment/facebookinstantgames-1.jpg b/deployment/facebookinstantgames-1.jpg
new file mode 100644
index 000000000..7e7ebdc6e
Binary files /dev/null and b/deployment/facebookinstantgames-1.jpg differ
diff --git a/deployment/facebookinstantgames-2.jpg b/deployment/facebookinstantgames-2.jpg
new file mode 100644
index 000000000..122a3dbb0
Binary files /dev/null and b/deployment/facebookinstantgames-2.jpg differ
diff --git a/deployment/facebookinstantgames-3.jpg b/deployment/facebookinstantgames-3.jpg
new file mode 100644
index 000000000..4157da4d6
Binary files /dev/null and b/deployment/facebookinstantgames-3.jpg differ
diff --git a/everywhere-actions.html b/everywhere-actions.html
new file mode 100644
index 000000000..c520f7e69
--- /dev/null
+++ b/everywhere-actions.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+ Everywhere Actions | Needle Engine Documentation
+
+
+
+
+
Needle's Everywhere Actions are a set of carefully chosen components that allow you to create interactive experiences in Unity without writing a single line of code. They are designed to serve as building blocks for experiences across the web, mobile and XR, including Augmented Reality on iOS.
From low-level triggers and actions, higher-level complex interactive behaviours can be built.
For iOS support add the USDZExporter component to your scene. It is good practice to add it to the same object as the WebXR component (but not mandatory)
To add an action to any object in your scene select it and then click Add Component > Needle > Everywhere Actions > [Action].
Demonstrates how to attach 3D content onto a custom image marker. Start the scene below in AR and point your phone's camera at the image marker on a screen, or print it out.
On Android: please turn on "WebXR Incubations" in the Chrome Flags. You can find those by pasting chrome://flags/#webxr-incubations into the Chrome browser address bar of your Android phone.
Creating new Everywhere Actions involves writing code for your action in TypeScript, which will be used in the browser and for WebXR, and using our TriggerBuilder and ActionBuilder API to create a matching setup for Augmented Reality on iOS via QuickLook. When creating custom actions, keep in mind that QuickLook has a limited set of features available. You can still use any code you want for the browser and WebXR, but the behaviour for QuickLook may need to be an approximation built from the available triggers and actions.
TIP
Often constructing specific behaviours requires thinking outside the box and creatively applying the available low-level actions. An example would be a "Tap to Place" action โ there is no raycasting or hit testing available in QuickLook, but you could cover the expected placement area with a number of invisible objects and use a "Tap" trigger to move the object to be placed to the position of the tapped invisible object.
Here's the implementation for HideOnStart as an example for how to create an Everywhere Action with implementations for both the browser and QuickLook:
Often, getting the right behaviour will involve composing higher-level actions from the available lower-level actions. For example, our "Change Material on Click" action is composed of a number of fadeActions and internally duplicates objects with different sets of materials each. By carefully constructing these actions, complex behaviours can be achieved.
To see the implementation of our built-in Everywhere Actions, please take look at src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts.
Add an ExportInfo component to your Unity scene to generate a new web project from a template, link to an existing web project that you want to export to, set up dependencies to other libraries and packages and to deploy your project.
By default, your scene is exported on save. This setting can be changed by disabling Auto Export in the ExportInfo component.
To export meshes, materials, animations, textures (...) create a new GameObject in your hierarchy and add a GltfObject component to it. This is the root of a new glTF file. It will be exported whenever you make a change to the scene and save.
Only scripts and data on and inside those root objects is exported. Scripts and data outside of them are not exported.
Add a cube as a child of your root object and save your scene. Note that the output assets/ folder (see project structure) now contains a new .glb file with the same name as your root GameObject.
You can enable the Smart Export setting (via Edit/Project Settings/Needle ) to only export when a change in this object's hierarchy is detected.
How to prevent specific objects from being exported
Objects with the EditorOnly tag will be ignored on export including their child hierarchy. Be aware that this is preferred over disabling objects as disabled will still get exported in case they're turned on later.
If you want to split up your application into multiple levels or scenes then you can simply use the SceneSwitcher component. You can then structure your application into multiple scenes or prefabs and add them to the SceneSwitcher array to be loaded and unloaded at runtime. This is a great way to avoid having to load all your content upfront and to keep loading times small (for example it is what we did on needle.toolsopen in new window by separating each section of your website into its own scene and only loading them when necessary)
Max. 500k vertices (less if you target mobile VR as well)
Max. 4x 2k lightmaps
You can split up scenes and prefabs into multiple glTF files, and then load those on demand (only when needed). This keeps loading performance fast and file size small. See the AssetReference section in the Scripting docs.
The scene complexity here is recommended to ensure good performance across a range of web-capable devices and bandwidths. There's no technical limitation to this beyond the capabilities of your device.
Prefabs can be exported as invidual glTF files and instantiated at runtime. To export a prefab as glTF just reference a prefab asset (from the project browser and not in the scene) from one of your scriptsopen in new window.
Exporting Prefabs works with nesting too: a component in a Prefab can reference another Prefab which will then also be exported. This mechanism allows for composing scenes to be as lightweight as possible and loading the most important content first and defer loading of additional content.
Similar to Prefab assets, you can reference other Scene assets. To get started, create a component in Unity with a UnityEditor.SceneAsset field and add it to one of your GameObjects inside a GltfObject. The referenced scene will now be exported as a separate glTF file and can be loaded/deserialized as a AssetReference from TypeScript.
You can keep working inside a referenced scene and still update your main exporter scene/website. On scene save or play mode change we will detect if the current scene is being used by your currently running server and then trigger a re-export for only that glb. (This check is done by name - if a glb inside your <web_project>/assets/ folder exists, it is exported again and the main scene reloads it.)
As an example on our websiteopen in new window each section is setup as a separate scene and on export packed into multiple glb files that we load on demand:
If you want to reference and load a prefab from one of your scripts you can declare a AssetReference type. Here is a minimal example:
import{ Behaviour, serializable, AssetReference }from"@needle-tools/engine";
+
+exportclassMyClassextendsBehaviour{
+
+ // if you export a prefab or scene as a reference from Unity you'll get a path to that asset
+ // which you can de-serialize to AssetReference for convenient loading
+ @serializable(AssetReference)
+ myPrefab?: AssetReference;
+
+ asyncstart(){
+ // directly instantiate
+ const myInstance =awaitthis.myPrefab?.instantiate();
+
+ // you can also just load and instantiate later
+ // const myInstance = await this.myPrefab.loadAssetAsync();
+ // this.gameObject.add(myInstance)
+ // this is useful if you know that you want to load this asset only once because it will not create a copy
+ // since ``instantiate()`` does create a copy of the asset after loading it
+ }
+}
+
One current limitation is that materials won't be duplicated on export โ if you want to animate the same material with different colors, for example, you currently need to split the material in two.
By default, materials are converted into glTF materials on export. glTF supports a physically based material model and has a number of extensions that help to represent complex materials.
For full control over what gets exported, it's highly recommended to use the glTF materials provided by UnityGltf:
PBRGraph
UnlitGraph
These materials are exported as-is, with no conversion necessary. They allow for using advanced material properties such as refractive transmission and iridescence, which can be exported as well.
Materials that can be converted out-of-the-box:
BiRP/Standard
BiRP/Autodesk Interactive
BiRP/Unlit
URP/Lit
URP/Unlit
Other materials are converted using a propery name heuristic. That means that depending on what property names your materials and shaders use, you might want to either refactor your custom shader's properties to use the property names of either URP/Lit or PBRGraph, or export the material as Custom Shader.
To export custom shaders (e.g. ShaderGraph shaders), add an ExportShader Asset Label (see bottom of the inspector) to the shader you want to export.
WARNING
Please see limitations listed below
Note that Custom Shaders aren't part of the ratified glTF material model. The resulting GLB files will not display correctly in other viewers (the materials will most likely display white).
We currently only support custom Unlit shaders โ Lit shader conversion is not officially supported.
Custom Lit Shaders are currently experimental. Not all rendering modes are supported.
Shadow receiving on custom shaders is not supported
Skinned meshes with custom shaders are not supported
As there's multiple coordinate system changes when going from Unity to three.js and glTF, there might be some changes necessary to get advanced effects to work. We try to convert data on export but may not catch all cases where conversions are necessary. These coordinate changes are
UV coordinates in Unity start at the bottom left; in glTF they start at the top left.
X axis values are flipped in glTF compared to Unity (a variant of a left-handed to right-handed coordinate system change).
When working on multiple scenes, disable "Auto Generate" and bake lightmaps explicitly. Otherwise, Unity will discard temporary lightmaps on scene change.
There's no 100% mapping between how Unity handles lights and environment and how three.js handle that. For example, Unity has entirely separate code paths for lightmapped and non-lightmapped objects (lightmapped objects don't receive ambient light since that is already baked into their maps), and three.js doesn't distinguish in that way.
This means that to get best results, we currently recommend specific settings if you're mixing baked and non-baked objects in a scene:
Environment Lighting: Skybox
+Ambient Intensity: 1
+Ambient Color: black
+
2021.3+
2020.3+
If you have no baked objects in your scene, then the following settings should also yield correct results:
This usually happens when you're using custom shaders or materials and their properties don't cleanly translate to known property names for glTF export. You can either make sure you're using glTF-compatible materials and shaders, or mark shaders as "custom" to export them directly.
Read more about recommended glTF workflows:
Read more about custom shaders:
# There's a SSL error when opening the local website
This is expected. We're enforcing HTTPS to make sure that WebXR and other modern web APIs work out-of-the-box, but that means some browsers complain that the SSL connection (between your local development server and the local website) can't be verified. It also prevents Automatic Reload from working on iOS and MacOS.
See the Testing docs for information on how to set up a self-signed certificate for a smoother development experience.
If that happens there's usually an exception either in engine code or your code. Open the dev tools (Ctrl + Shift + I or F12 in Chrome) and check the Console for errors. In some cases, especially when you just updated the Needle Engine package version, this can be fixed by stopping and restarting the local dev server. For that, click on the running progress bar in the bottom right corner of the Editor, and click the little X to cancel the running task. Then, simply press Play again.
# How to fix Uncaught ReferenceError: NEEDLE_ENGINE_META is not defined / NEEDLE_USE_RAPIER is not defined
If you are using vite or next.js make sure to add the Needle Engine plugins to your config. Example for vite:
# THREE.EXRLoader: provided file doesnt appear to be in OpenEXR format
Please make sure that sure that you have set Lightmap Encoding to Normal Quality. Go to Edit/Project Settings/Player for changing the setting.
# My website becomes too large / is loading slow (too many MB)
This can have many reasons, but a few common ones are:
too many textures or textures are too large
meshes have too many vertices
meshes have vertex attributes you don't actually need (e.g. have normals and tangents but you're not using them)
objects are disabled and not ignored โ disabled objects get exported as well in case you want to turn them on at runtime! Set their Tag to EditorOnly to completely ignore them for export.
you have multiple GltfObject components in your scene and they all have EmbedSkybox enabled (you need to have the skybox only once per scene you export)
If loading time itself is an issue you can try to split up your content into multiple glb files and load them on-demand (this is what we do on our website). For it to work you can put your content into Prefabs or Scenes and reference them from any of your scripts. Please have a look at Scripting/Addressables in the documentation.
# My scene is too bright / lighting looks different than in Unity
Make sure that your lights are set to "Baked" or "Realtime". "Mixed" is currently not supported.
Lights set to mixed (with lightmapping) do affect objects twice in three.js, since there is currently no way to exclude lightmapped objects from lighting
The Intensity Multiplier factor for Skybox in Lighting/Environment is currently not supported and has no effect in Needle Engine
Light shadow intensity can currently not be changed due to a three.js limitation.
Your light has shadows enabled (either Soft Shadow or Hard Shadow)
Your objects are set to "Cast Shadows: On" (see MeshRenderer component)
For directional lights the position of the light is currently important since the shadow camera will be placed where the light is located in the scene.
# I'm using networking and Glitch and it doesn't work if more than 30 people visit the Glitch page at the same time
Deploying on Glitch is a fast way to prototype and might even work for some small productions. The little server there doesn't have the power and bandwidth to host many people in a persistent session.
We're working on other networking ideas, but in the meantime you can host the website somewhere else (with node.js support) or simply remix it to distribute load among multiple servers. You can also host the networking backend packageopen in new window itself somewhere else where it can scale e.g. Google Cloud.
Make sure to add the WebXR component somewhere inside your root GltfObject.
Optionally add a AR Session Root component on your root GltfObject or within the child hierarchy to specify placement, scale and orientation for WebXR.
Optionally add a XR Rig component to control where users start in VR
# I created a new script in a sub-scene but it does not work
When creating new scripts in npmdefs in sub-scenes (that is a scene that is exported as a reference from a script in your root export scene) you currently have to re-export the root scene again. This is because the code-gen that is responsible for registering new scripts currently only runs for scenes with a ExportInfo component. This will be fixed in the future.
# My local server does not start / I do not see a website
The most likely reason is an incorrect installation. Check the console and the ExportInfo component for errors or warnings.
If these warnings/errors didn't help, try the following steps in order. Give them some time to complete. Stop once your problem has been resolved. Check the console for warnings and errors.
Install your project by selecting your ExportInfo component and clicking Install
Run a clean installation by selecting your ExportInfo component, holding Alt and clicking Clean Install
Try opening your web project directory in a command line tool and follow these steps:
run npm install and then npm run dev-host
Make sure both the local runtime package (node_modules/@needle-tools/engine) as well as three.js (node_modules/three) did install.
You may run npm install in both of these directories as well.
# Does C# component generation work with javascript only too?
While generating C# components does technically run with vanilla javascript too we don't recommend it and fully support it since it is more guesswork or simply impossible for the generator to know which C# type to create for your javascript class. Below you find a minimal example on how to generate a Unity Component from javascript if you really want to tho.
On Windows: Make sure you have added toktx to your system environment variables. You may need to restart your computer after adding it to refresh the environment variables. The default install location is C:\Program Files\KTX-Software\bin
# Installing the web project takes forever / does never finish / EONET: no such file or directory
Make sure to not create a project on a drive formatted as exFAT because exFAT does not support symlinks, which is required for Needle Engine for Unity prior to version 3.x. You can check the formatting of your drives using the following steps:
Open "System Information" (either windows key and type that or enter "msinfo32" in cmd)
Select Components > Storage > Drives
Select all (Ctrl + A) on the right side of the screen and copy that (Ctrl + C) and paste here (Ctrl + V)
# NPM install fails and there are errors about hard drive / IO
Make sure your project is on a disk that is known to work with node.js. Main reason for failures is that the disk doesn't support symlinks (symbolic links / softlinks), which is a requirement for proper functioning of node.js. NTFS formatting should always work. Known problematic file system formattings are exFAT and FAT32.
To check the format of your drives, you can:
Open "System Information" (either Windows key and type "System Information" or enter msinfo32 in cmd Windows + R)
Select "Components > Storage > Drives"
There, you can see all drives and their formatting listed. Put your projects on a drive that is NTFS formatted.
# I'm getting errors with "Unexpected token @. Expected identifier, string literal, numeric literal or ..."
Needle Engine uses typescript decorators for serialization. To fix this error make sure to enable experimentalDecorators in your tsconfig.json
# I'm getting an error 'failed to load config ... vite.config.js' when running npm commands on Mac OS
You're likely using an x86_64 version of Unity on an (ARM) Apple Silicon processor. Unity 2020.3 is only available for x86_64, later versions also have Apple Silicon versions. Our Unity integration calling npm will thus do so from an x86_64 process, resulting in the x86_64 version of node and vite/esbuild being used. When you afterwards try to run npm commands in the same project from an Apple Silicon app (e.g. VS Code), npm will complain about mismatching architectures with a long error message.
To fix this, use an Apple Silicon version of Unity (2021.1 or later).
You can also temporarily fix it on 2020.3 by deleting the node_modules folder and running npm install again from VS Code. You'll have to delete node_modules again when you switch back to Unity.
This can happen when you have e.g. a SceneSwitcher (or any other component that loads a scene or asset) and the referenced Asset in Unity contains a GltfObject that has the same name as your original scene with the SceneSwitcher. You can double check this in Unity if you get an error that says something like:
Failed to export โ YourSceneName.glb
+you seem to have objects with the same name referencing each other.
+
To fix this you can:
Remove the GltfObject in the referenced Prefab or Scene
Rename the GameObject with the component that loads the referenced scenes
Use a detector like this oneopen in new window to determine if your device supports WebGL 2, it also hints at what could be the cause of your problem, but generally make sure you have updated your browser and drivers. WebGL 1 is not supported.
Both PBR Materials and Custom shaders created with Shader Graph or other systems can be exported.
Use the node based ShaderGraphopen in new window to create shaders for the web. ShaderGraph makes it easy for artists to keep creating without having to worry about syntax.
Needle Engine runs everywhere web technology does: run the same application on desktop, mobile, AR or VR. We build Needle Engine with XR in mind and consider this as and integral part of responsive webdesign!
Lightmaps can be baked in Unity or Blender to easily add beautiful static light to your 3d content. Lightbaking for the web was never as easy. Just mark objects that you want to lightmap as static in Unity, add one or many lights to your scene (or use emissive materials) and click bake. Needle Engine will export your lightmaps per scene and automatically load and display them just as you see it in the Editor!
Note: There is no technical limitation on which lightmapper to use, as long as they end up in Unity's lightmapping data structures. Third party lightmappers such as Bakeryopen in new window thus are also supported.
Networking is built into the core runtime. Needle Engine deployments to Glitch come with a tiny server that allows you to deploy a multiplayer 3D environment in seconds. The built-in networked components make it easy to get started, and you can create your own synchronized components. Synchronizing variables and state is super easy!
Needle Engine brings powerful animations, state control and sequencing to the web โ from just playing a single animation to orchestrating and blending complex animations and character controllers. The Exporter can translate Unity components like Animator and Timeline into a web-ready format. We even added this functionality to our Blender addon so you can craft compatible animation state machines and export nla tracks as timelines to the web from within Blender too.
The Animator and AnimatorControlleropen in new window components in Unity let you setup animations and define conditions for when and how to blend between them. We support exporting state machines, StateMachineBehaviours, transitions and layers. StateMachineBehaviours are also supported with OnStateEnter, OnStateUpdate and OnStateExit events.
Note: Sub-states and Blend Trees are not supported.
We're also translating Unity's Timelineopen in new window setup and tracks into a web-ready format. Supported tracks include: AnimationTrack, AudioTrack, ActivationTrack, ControlTrack, SignalTrack.
Export of Unity ParticleSystem (Shuriken) is in development. Features currently include world/local space simulation, box and sphere emitter shapes, emission over time as well as burst emission, velocity- and color over time, emission by velocity, texturesheet animation, basic trails. See a live sampleopen in new window of supported features below:
Builtin effects include Bloom, Screenspace Ambient Occlusion, Depth of Field, Color Correction. You can also create your own custom effects. See the component reference for a complete list.
Needle Engine comes with powerful integrations into the Unity Editor and Blender. It allows you to setup and export complex scenes in a visual way providing easy and flexible collaboration between artists and developers.
Needle Engine provides a tight integration into the Unity Editor. This allows developers and designers alike to work together in a familiar environment and deliver fast, performant and lightweight web-experiences.
The following guide is mainly aimed at developers with a Unity3D background but it may also be useful for developers with a web or three.js background. It covers topics regarding how things are done in Unity vs in three.js or Needle Engine.
If you are all new to Typescript and Javascript and you want to dive into writing scripts for Needle Engine then we also recommend reading the Typescript Essentials Guide for a basic understanding between the differences between C# and Javascript/Typescript.
Needle Engine is a 3d web engine running on-top of three.jsopen in new window. Three.js is one of the most popular 3D webgl based rendering libraries for the web. Whenever we refer to a gameObject in Needle Engine we are actually also talking about a three.js Object3D, the base type of any object in three.js. Both terms can be used interchangeably. Any gameObjectis a Object3D.
This also means that - if you are already familiar with three.js - you will have no problem at all using Needle Engine. Everything you can do with three.js can be done in Needle Engine as well. If you are already using certain libraries then you will be able to also use them in a Needle Engine based environment.
Note: Needle Engine's Exporter does NOT compile your existing C# code to Web Assembly. While using Web Assembly may result in better performance at runtime, it comes at a high cost for iteration speed and flexibility in building web experiences. Read more about our vision and technical overview.
How to create a new Unity project with Needle Engine? (Video)
If you have seen some Needle Engine scripts then you might have noticed that some variables are annotated with @serializable above their declaration. This is a Decorator in Typescript and can be used to modify or annotate code. In Needle Engine this is used for example to let the core serialization know which types we expect in our script when it converts from the raw component information stored in the glTF to a Component instance. Consider the following example:
This tells Needle Engine that myOtherComponent should be of type Behaviour. It will then automatically assign the correct reference to the field when your scene is loaded. The same is true for someOtherObject where we want to deserialize to an Object3D reference.
Field without any accessor modified like private, public or protected will by default be public in javascript
/// no accessor means it is public:
+myNumber?:number;
+// explicitly making it private:
+private myPrivateNumber?:number;
+protected myProtectedNumber?:number;
+
To access the current scene from a component you use this.scene which is equivalent to this.context.scene, this gives you the root three.js scene object.
To traverse the hierarchy from a component you can either iterate over the children of an object with a for loop:
for(let i =0; i <this.gameObject.children; i++)
+ const ch =this.gameObject.children[i];
+
Another option that is quite useful when you just want to iterate objects being renderable you can query all renderer components and iterate over them like so:
Needle Engine is making heavy use of a Component System that is similar to that of Unity. This means that you can add or remove components to any Object3D / GameObject in the scene. A component will be registered to the engine when using addNewComponent(<Object3D>, <ComponentType>). The event methods that the attached component will then automatically be called by the engine (e.g. update or onBeforeRender). A full list of event methods can be found in the scripting documentation
For getting component you can use the familiar methods similar to Unity. Note that the following uses the Animator type as an example but you can as well use any component type that is either built-in or created by you.
Method name
Desciption
this.gameObject.getComponent(Animator)
Get the Animator component on a GameObject/Object3D. It will either return the Animator instance if it has an Animator component or null if the object has no such componnent.
this.gameObject.getComponentInChildren(Animator)
Get the first Animator component on a GameObject/Object3D or on any of its children
this.gameObject.getComponentsInParents(Animator)
Get all animator components in the parent hierarchy (including the current GameObject/Object3D)
These methods are also available on the static GameObject type. For example GameObject.getComponent(this.gameObject, Animator) to get the Animator component on a passed in GameObject/Object3D.
To search the whole scene for one or multiple components you can use GameObject.findObjectOfType(Animator) or GameObject.findObjectsOfType(Animator).
Some Unity-specific types are mapped to different type names in our engine. See the following list:
Type in Unity
Type in Needle Engine
UnityEvent
EventList
A UnityEvent will be exported as a EventList type (use serializable(EventList) to deserialize UnityEvents)
GameObject
Object3D
Transform
Object3D
In three.js and Needle Engine a GameObject and a Transform are the same (there is no Transform component). The only exception to that rule is when referencing a RectTransform which is a component in Needle Engine as well.
Color
RGBAColor
The three.js color type doesnt have a alpha property. Because of that all Color types exported from Unity will be exported as RGBAColor which is a custom Needle Engine type
The major difference here to keep in mind is that position in three.js is by default a localspace position whereas in Unity position would be worldspace. The next section will explain how to get the worldspace position in three.js.
In three.js (and thus also in Needle Engine) the object.position, object.rotation, object.scale are all local space coordinates. This is different to Unity where we are used to position being worldspace and using localPosition to deliberately use the local space position.
If you want to access the world coordinates in Needle Engine we have utility methods that you can use with your objects. Call getWorldPosition(yourObject) to calculate the world position. Similar methods exist for rotation/quaternion and scale. To get access to those methods just import them from Needle Engine like so import { getWorldPosition } from "@needle.tools/engine"
Note that these utility methods like getWorldPosition, getWorldRotation, getWorldScale internally have a buffer of Vector3 instances and are meant to be used locally only. This means that you should not cache them in your component, otherwise your cached value will eventually be overriden. But it is safe to call getWorldPosition multiple times in your function to make calculations without having to worry to re-use the same instance. If you are not sure what this means you should take a look at the Primitive Types section in the Typescript Essentials Guide
Use this.context.physics.raycast() to perform a raycast and get a list of intersections. If you dont pass in any options the raycast is performed from the mouse position (or first touch position) in screenspace using the currently active mainCamera. You can also pass in a RaycastOptions object that has various settings like maxDistance, the camera to be used or the layers to be tested against.
Note that the calls above are by default raycasting against visible scene objects. That is different to Unity where you always need colliders to hit objects. The default three.js solution has both pros and cons where one major con is that it can perform quite slow depending on your scene geometry. It may be especially slow when raycasting against skinned meshes. It is therefor recommended to usually set objects with SkinnedMeshRenderers in Unity to the Ignore Raycast layer which will then be ignored by default by Needle Engine as well.
Another option is to use the physics raycast methods which will only return hits with colliders in the scene.
const hit =this.context.physics.engine?.raycast();
+
You can also subscribe to events in the InputEvents enum like so:
import{ Behaviour, InputEvents }from"@needle-tools/engine";
+
+exportclassMyScriptextendsBehaviour
+{
+ onEnable(){
+ this.context.input.addEventListener(InputEvents.PointerDown,this.onPointerDown);
+ }
+ onDisable(){
+ // it is recommended to also unsubscribe from events when your component becomes inactive
+ this.context.input.removeEventListener(InputEvents.PointerDown,this.onPointerDown);
+ }
+
+ privateonPointerDown=(evt)=>{console.log(evt);}
+}
+
If you want to handle inputs yourself you can also subscribe to all events the browser providesopen in new window (there are a ton). For example to subscribe to the browsers click event you can write:
Note that in this case you have to handle all cases yourself. For example you may need to use different events if your user is visiting your website on desktop vs mobile vs a VR device. These cases are automatically handled by the Needle Engine input events (e.g. PointerDown is raised both for mouse down, touch down and in case of VR on controller button down).
The Debug.Log() equivalent in javascript is console.log(). You can also use console.warn() or console.error().
console.log("Hello web");
+// You can pass in as many arguments as you want like so:
+console.log("Hello", someVariable, GameObject.findObjectOfType(Renderer),this.context);
+
In Unity you normally have to use special methods to draw Gizmos like OnDrawGizmos or OnDrawGizmosSelected. In Needle Engine on the other hand such methods dont exist and you are free to draw gizmos from anywhere in your script. Note that it is also your responsibility then to not draw them in e.g. your deployed web application (you can just filter them by if(isDevEnvironment))).
Here is an example to draw a red wire sphere for one second for e.g. visualizing a point in worldspace
Import from @needle-tools/engine e.g. import { getParam } from "@needle-tools/engine"
Method name
Description
getParam()
Checks if a url parameter exists. Returns true if it exists but has no value (e.g. ?help), false if it is not found in the url or is set to 0 (e.g. ?help=0), otherwise it returns the value (e.g. ?message=test)
isMobileDevice()
Returns true if the app is accessed from a mobile device
isDevEnvironment()
Returns true if the current app is running on a local server
In C# you usually work with a solution containing one or many projects. In Unity this solution is managed by Unity for you and when you open a C# script it opens the project and shows you the file. You usually install Packages using Unity's built-in package manager to add features provided by either Unity or other developers (either on your team or e.g. via Unity's AssetStore). Unity does a great job of making adding and managing packages easy with their PackageManager and you might never have had to manually edit a file like the manifest.json (this is what Unity uses to track which packages are installed) or run a command from the command line to install a package.
In a web environment you use npm - the Node Package Manager - to manage dependencies / packages for you. It does basically the same to what Unity's PackageManager does - it installs (downloads) packages from some server (you hear it usually called a registry in that context) and puts them inside a folder named node_modules.
When working with a web project most of you dependencies are installed from npmjs.comopen in new window. It is the most popular package registry out there for web projects.
Here is an example of how a package.json might look like:
Our default template uses Vite as its bundler and has no frontend framework pre-installed. Needle Engine is unoppionated about which framework to use so you are free to work with whatever framework you like. We have samples for popular frameworks like Vue.js, Svelte, Next.js, React or React Three Fiber.
To install a dependency from npm you can open your web project in a commandline (or terminal) and run npm i <the/package_name> (shorthand for npm install) For example run npm i @needle-tools/engine to install Needle Engineopen in new window. This will then add the package to your package.json to the dependencies array. To install a package as a devDependency only you can run npm i --save-dev <package_name>. More about the difference between dependencies and devDependencies below.
# What's the difference between 'dependencies' and 'devDependencies'
You may have noticed that there are two entries containing dependency - dependencies and devDependencies.
dependencies are always installed (or bundled) when either your web project is installed or in cases where you develop a library and your package is installed as a dependency of another project.
devDependencies are only installed when developing the project (meaning that when you directly run install in the specific directory) and they are otherwise not included in your project.
# How do I install another package or dependency and how to use it?
The Installing section taught us that you can install dependencies by running npm i <package_name> in your project directory where the package_name can be any package that you find on npm.jsopen in new window.
Then open one of your script files in which you want to use tweening and import at the top of the file:
import*asTWEENfrom'@tweenjs/tween.js';
+
Note that we do here import all types in the library by writing * as TWEEN. We could also just import specific types like import { Tween } from @tweenjs/tween.js.
Now we can use it in our script. It is always recommended to refer to the documentation of the library that you want to use. In the case of tween.js they provide a user guideopen in new window that we can follow. Usually the Readme page of the package on npm contains information on how to install and use the package.
To rotate a cube we create a new component type called TweenRotation, we then go ahead and create our tween instance for the object rotation, how often it should repeat, which easing to use, the tween we want to perform and then we start it. We then only have to call update every frame to update the tween animation. The final script looks like this:
exportclassTweenRotationextendsBehaviour{
+
+ // save the instance of our tweener
+ private _tween?:TWEEN.Tween<any>;
+
+ start(){
+ // create the tween instance
+ this._tween =newTWEEN.Tween(this.gameObject.rotation);
+ // set it to repeat forever
+ this._tween.repeat(Infinity);
+ // set the easing to use
+ this._tween.easing(TWEEN.Easing.Quintic.InOut);
+ // set the values to tween
+ this._tween.to({ y: Math.PI*0.5},1000);
+ // start it
+ this._tween.start();
+ }
+
+ update(){
+ // update the tweening every frame
+ // the '?' is a shorthand for checking if _tween has been created
+ this._tween?.update();
+ }
+}
+
Now we only have to add it to any of the objects in our scene to rotate them forever. You can see the final script in action hereopen in new window.
Drop the downloaded .unitypackage file into a Unity project (or double click to open) and confirm that you want to import it.
Wait for the installation and import to finish.
A window may open stating that "A new scoped registry is now available in the Package Manager.". This is our Needle Package registry where packages are downloaded from. You can safely close that window and continue with the next step below.
Continue....
Option 1: Explore Samples Select Needle Engine > Explore Samples to view, open and modify all available sample scenesopen in new window.
Option 2: Create a new scene from a template Select File > New Scene and choose from one of the Needle templates. We recommend the Collaborative Sandboxopen in new window template which is a great way to get started with interactivity, multiplayer,and adding assets.
Video: Starting from a fresh Unity projectCreate a new scene from a Scene Template
We provide a number of Scene Templates for quickly starting new projects. These allow you to go from idea to prototype in a few clicks.
Click on File > New Scene
Select one of the templates with (needle) in their name and click Create.
Click Play to install and startup your new web project.
Create a new scene from scratch
If you don't want to start from a scene template, you can follow these steps. Effectively, we're going to recreate the "Minimal (Needle)" template that's shipping with the package.
Create a new empty scene
Set up your scene for exporting Add an empty GameObject, name it "Exporter" and add an ExportInfo component to it. In this component you create and quickly access your exported runtime project. It also warns you if any of our packages and modules are outdated or not locally installed in your web project.
Note
By default, the project name matches the name of your scene. If you want to change that, you can enter a Directory Name where you want to create your new runtime project. The path is relative to your Unity project.
Choose a web project template Now, select a web project template for your project. The default template is based on Viteopen in new window, a fast web app bundler.
Click Play to install and start your new web project
With Needle Engine for Blender you can build fully interactive 3D websites running on three.js. They can easily deployed to the web and get optimized automatically by the Needle Engine Build Pipeline (needs Node.js installed)
The Blender addon is downloaded as a zip file.
In Blender go to File / Settings / Add-ons and click the Install button.
Needle Engine is built ontop of three.js. If you just want to work with the Needle Engine runtime and don't want to use any Editor integration just yet then go ahead and install it from npm by running:
The local website shows a warning: website not secure
You might see a warning in your browser about SSL Security depending on your local configuration. This is because while the connection is encrypted, by default there's no SSL certificate that the browser can validate. If that happens: click Advanced and Proceed to Site. Now you should see your scene in the browser!
Something is not working as expected? Where can I see logs?
Keep an eye for console warnings! We log useful details about recommended project settings and so on. For example, your project should be set to Linear color space (not Gamma), and we'll log an error to the Unity console if that's not the case.
Please also have a look at our FAQ if your question is not answered here.
The following guide tries to highlight some of the key differences between C#, Javascript and Typescript. This is most useful for developers new to the web ecosystem.
Here are also some useful resources for learning how to write Typescript:
# Key differences between C#, Javascript or Typescript
CSharp or C# is a statically typed & compiled language. It means that before your code can run (or be executed) it has to be compiled - translated - into IL or CIL, an intermediate language that is a little closer to machine code. The important bit to understand here is that your code is analyzed and has to pass certain checks and rules that are enforced by the compiler. You will get compiler errors in Unity and your application not even start running if you write code that violates any of the rules of the C# language. You will not be able to enter Play-Mode with compiler errors.
Javascript on the other hand is interpreted at runtime. That means you can write code that is not valid and cause errors - but you will not see those errors until your program runs or tries to execute exactly that line that has the error. For example you can write var points = 100; points += "hello world"; and nobody will complain until you run the code in a browser.
Typescript is a language designed by Microsoft that compiles to javascript It adds a lot of features like for example type-safety. That means when you write code in Typescript you can declare types and hence get errors at compile-time when you try to e.g. make invalid assignments or call methods with unexpected types. Read more about types in Javascript and Typescript below.
Vanilla Javascript does (as of today) not have any concept of types: there is no guarantuee that a variable that you declared as let points = 100 will still be a number later in your application. That means that in Javascript it is perfectly valid code to assign points = new Vector3(100, 0, 0); later in your code. Or even points = null or points = myRandomObject - you get the idea. This is all OK while you write the code but it may crash horrible when your code is executed because later you write points -= 1 and now you get errors in the browser when your application is already running.
As mentioned above Typescript was created to help fix that problem by adding syntax for defining types.
It is important to understand that you basically still write Javascript when you write Typescript and while it is possible to circumvent all type checking and safety checks by e.g. adding //@ts-ignore above a erroneous line or defining all types as any this is definitely not recommneded. Types are here to help you find errors before they actually happen. You really dont want to deploy your website to your server only to later get reports from users or visitors telling you your app crashed while it was running.
While vanilla Javascript does not offer types you can still add type-annotations to your javascript variables, classes and methods by using JSDocopen in new window.
In C# you write variables either by using the type or the var keyword. For example you can either write int points = 100; or alternatively use var and let the compiler figure out the correct type for you: var points = 100
In Javascript or Typescript you have two modern options to declaring a variable. For a variable that you plan to re-assign use let, for example let points = 100; For a variable that you do not want to be able to re-assign use const, for example const points = 100;
Be aware of var You might come across the var keyword in javascript as well but it is not recommended to use it and the modern replacement for it is let. Learn more about var vs letopen in new window.
Please note that you can still assign values to variables declared with const if they are (for example) a custom type. Consider the following example:
const myPosition : Vector3 =newVector3(0,0,0);
+myPosition.x =100;// Assigning x is perfectly fine
+
The above is perfectly fine Typescript code because you don't re-assign myPosition but only the x member of myPosition. On the other hand the following example would not be allowed and cause a runtime or typescript error:
const myPosition : Vector3 =newVector3(0,0,0);
+myPosition =newVector3(100,0,0);// โ ASSIGNING TO CONST IS NOT ALLOWED
+
In Unity you usually add using statements at the top of you code to import specific namespaces from Assemblies that are references in your project or - in certain cases - you migth find yourself importing a specific type with a name from a namespace. See the following example:
usingUnityEngine;
+// importing just a specific type and giving it a name
+usingMonoBehaviour=UnityEngine.MonoBehaviour;
+
This is how you do the same in Typescript to import specific types from a package:
Vector2, Vector3, Vector4... If you have a C# background you might be familiar with the difference between a class and a struct. While a class is a reference type a struct is a custom value type. Meaning it is, depending on the context, allocated on the stack and when being passed to a method by default a copy is created. Consider the following example in C#:
voidMyCallerMethod(){
+ var position =newVector3(0,0,0);
+ MyExampleVectorMethod(position);
+ UnityEngine.Debug.Log("Position.x is "+ position.x);// Here x will be 0
+}
+voidMyExampleVectorMethod(Vector3 position){
+ position.x =42;
+}
+
A method is called with a Vector3 named position. Inside the method the passed in vector position is modified: x is set to 42. But in C# the original vector that is being passed into this method (see line 2) is not changed and x will still be 0 (line 4).
The same is not true for Javascript/Typescript. Here we don't have custom value types, meaning if you come across a Vector in Needle Engine or three.js you will always have a reference type. Consider the following example in typescript:
import{ Vector3 }from"three"
+functionmyCallerMethod():void{
+ const position =newVector(0,0,0);
+ myExampleVectorMethod(position);
+ console.log("Position.x is "+ position.x);// Here x will be 42
+}
+functionmyExampleVectorMethod(position: Vector3):void{
+ position.x =42;
+}
+
Do you see the difference? Because vectors and all custom objects are in fact reference types we will have modified the original position variable (line 3) and x is now 42.
This is not only important to understand for methods but also when working with variables. In C# the following code will produce two instances of Vector3 and changing one will not affect the other:
var myVector =newVector3(1,1,1);
+var myOtherVector = myVector;
+myOtherVector.x =42;
+// will log: 1, 42
+UnityEngine.Debug.Log(myVector.x +", "+ myOtherVector.x);
+
If you do the same in Typescript you will not create a copy but get a reference to the same myVector instance instead:
While in C# you can use operator overloading this is not available in Javascript unfortunately. This means that while you can multiply a Vector3 in C# like this:
var myFirstVector =newVector3(1,1,1);
+var myFactor =100f;
+myFirstVector *= myFactor;
+// โ myFirstVector is now 100, 100, 100
+
you have to use a method on the Vector3 type to archieve the same result (just with a little more boilerplate code)
const myFirstVector : Vector3 =newVector3(1,1,1)
+const myFactor = 100f;
+myFirstVector.multiplyScalar(myFactor);
+// โ myFirstVector is now 100, 100, 100
+
You notice that the second variable playerIsNullOrUndefined is using == which does a loose equality check in which case null and undefined will both result in truehere. You can read more about that hereopen in new window
When you subscribe to an Event in C# you do it like this:
// this is how an event is declared
+eventAction MyEvent;
+// you subscribe by adding to (or removing from)
+voidOnEnable(){
+ MyEvent += OnMyEvent;
+}
+voidOnDisable(){
+ MyEvent -= OnMyEvent;
+}
+voidOnMyEvent(){}
+
In Typescript and Javascript when you add a method to a list you have to "bind this". That essentially means you create a method where you explictly set this to (usually) your current class instance. There are two way to archieve this.
Please note that we are using the type EventList here which is a Needle Engine type to declare events (the EventList will also automatically be converted to a UnityEvent and or a event list in Blender when you use them with our Editor integrations)
myEvent?: EventList;
+voidonEnable(){
+ this.myEvent.addEventListener(this.onMyEvent);
+}
+voidonDisable(){
+ this.myEvent.removeEventListener(this.onMyEvent);
+}
+// Declaring the function as an arrow method
+// to automatically bind this:
+privateonMyEvent=()=>{
+ console.log(this!==undefined,this)
+ }
+
There is also the more verbose "classical" way to archieve the same thing by manually binding this (and saving the method in a variable to later remove it again from the event list):
myEvent?: EventList;
+private _onMyEventFn?:Function;
+voidonEnable(){
+ // bind this
+ this._onMyEventFn =this.onMyEvent.bind(this);
+ // add the bound method to the event
+ this.myEvent?.addEventListener(this._onMyEventFn);
+}
+voidonDisable(){
+ this.myEvent?.removeEventListener(this._onMyEventFn);
+}
+
Needle Engine is build as a web component. This means just install @needle-tools/engine in your project and include <needle-engine src="path/to/your.glb"> anywhere in your web-project.
Install using npm: npm i @needle-tools/engine
With our default Vite based project template Needle Engine gets bundled into a web app on deployment. This ensures smaller files, tree-shaking (similar to code stripping in Unity) and optimizes load times. Instead of downloading numerous small scripts and components, only one or a few are downloaded that contain the minimal code needed.
Needle Engine is unoponiated about the choice of framework. The default template only uses vite as bundler. Adding vue to that is easy (see the vite docsopen in new window), we also provide an (experimental) react-three-fiber template and there should be nothing stopping your from using simpler or more complex frameworks.
Here's some example tech stacks that are possible and that we use Needle Engine with:
Vite + HTML โ It is what our default template uses
CDN without any bundler โ Find a code example here
In short: we're currently providing a minimal vite template, but you can extend it or switch to other frameworks โ Let us know what and how you build, and how we can improve the experience for your usecase or provide an example!
TIP
Some frameworks require custom settings in needle.config.json. Learn more here. Typically, the baseUrl needs to be set.
How do I create a custom project template in Unity?
You can create and share your own web project templates to use other bundlers, build systems, or none at all.
Create a new Template
Select Create/Needle Engine/Project Template to add a ProjectTemplate into the folder you want to use as a template
Done! It's that simple.
The dependencies come from unity when there is a NpmDef in the project (so when your project uses local references). You could also publish your packages to npm and reference them via version number.
Tree shaking refers to a common practice when it comes to bundling of web applications (see MSDN docsopen in new window). It means that code paths and features that are not used in your code will be removed from the final bundled javascript file(s) to reduce filesize. See below about features that Needle Engine includes and remove them:
How to remove Rapier physics engine? (Reduce the overall bundle size removing ~2MB (~600KB when gzipping))
Option 1: via needlePlugins config: Set useRapier to false in your vite.config: needlePlugins(command, needleConfig, { useRapier: false }),
Option 2: via vite.define config: Declare the NEEDLE_USE_RAPIER define with false
define:{
+ NEEDLE_USE_RAPIER:false
+},
+
Option 3: via .env Create a .env file in your web project and add VITE_NEEDLE_USE_RAPIER=false
Option 4: via Unity component Add the Needle Engine Modules component to your scene and set Physics Engine to None
Modify .vite.config.js and pass the options both to the needlePlugins as well as the VitePWA method (see code below)
import{ VitePWA }from'vite-plugin-pwa';
+
+exportdefaultdefineConfig(async({ command })=>{
+
+ // create the PWAOptions object.
+ // You can edit or enter PWA settings here (e.g. change the PWA name or add icons).
+ /** @type{import("vite-plugin-pwa").VitePWAOptions} */
+ const PWAOptions ={};
+
+ const{ needlePlugins }=awaitimport("@needle-tools/engine/plugins/vite/index.js");
+
+ return{
+ plugins:[
+ // pass the PWAOptions object to the needlePlugins and the VitePWA function
+ needlePlugins(command, needleConfig,{pwaOptions: PWAOptions }),
+ VitePWA(PWAOptions),
+ ],
+ // the rest of your vite config...
+
# Accessing Needle Engine and Components from external javascript
Code that you expose can be accessed from JavaScript after bundling. This allows to build viewers and other applications where there's a split between data known at edit time and data only known at runtime (e.g. dynamically loaded files, user generated content). For accessing components from regular javascript outside of the engine please refer to the interop with regular javascript section
The needle-engine loading appearance can use a light or dark skin. To change the appearance use the loading-style attribute on the <needle-engine> web component. Options are light and dark (default):
Needle Engine is a web engine for complex and simple 3D applications alike. Run on your machine and deploy anywhere. It is flexible, extensible and has built-in support for collaboration and XR! It is built around the glTF standard for 3D assets. Powerful integrations for Unity and Blender allow artists and developers to collaborate and manage web applications inside battle-tested 3d editors. Needle Engine Integrations allow you to use Editor features for exporting models, author materials, animate and sequence animations, bake lightmaps and more with ease. Our powerful compression and optimization pipeline for the web make sure your files are ready, small and load fast!
Unbelievable Unity editor integration by an order of magnitude, and as straightforward as the docs claim. Wow. โ Chris Mahoney
This is the best thing I have seen after cinemachine in Unity โ Rinesh Thomas
Spent the last 2.5 months building this game, never built a game or used Unity before but absolutely loving the whole process. So rapid! โ Matthew Pieri
needle.tools is a wonderful showcase of what Needle contributes to 3D via the web. I just love it. โ Kevin Curry
Played with this a bit this morning ๐คฏ๐คฏ pretty magical โ Brit Gardner
This is huge for WebXR and shared, immersive 3D experiences! The AR part worked flawlessly on my Samsung S21. โ Marc Wakefield
This is amazing and if you are curious about WebXR with Unity this will help us get there โ Dilmer Valecillos
We just gotta say WOW ๐คฉ โ Unity for Digital Twins
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sy but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looIt uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo
There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage
of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo
but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which loo
+
+
+
diff --git a/meta/samples.json b/meta/samples.json
new file mode 100644
index 000000000..1fbe65c2f
--- /dev/null
+++ b/meta/samples.json
@@ -0,0 +1 @@
+{"webxr":[{"name":"webxr","page":"_meta-test","anchor":"my-test-header","absolute-url":"https://engine.needle.tools/docs/_meta-test#my-test-header","description":"here is a description","tags":["webxr","webgl","augmented reality"]}],"ui canvas":[{"name":"ui canvas","page":"_meta-test","anchor":"ui-canvas","absolute-url":"https://engine.needle.tools/docs/_meta-test#ui-canvas","description":"this is a description about UI","tags":["hello world"]}],"physics playground":[{"name":"physics playground","page":"_meta-test","anchor":"how-physics-works","absolute-url":"https://engine.needle.tools/docs/_meta-test#how-physics-works","tags":["physics"]}],"another sample":[{"name":"another sample","page":"_meta-test","anchor":"how-physics-works","absolute-url":"https://engine.needle.tools/docs/_meta-test#how-physics-works","tags":["physics"]}]}
\ No newline at end of file
diff --git a/meta/tags.json b/meta/tags.json
new file mode 100644
index 000000000..51641ef62
--- /dev/null
+++ b/meta/tags.json
@@ -0,0 +1 @@
+{"webxr":[{"page":"_meta-test","anchor":"my-test-header","absolute-url":"https://engine.needle.tools/docs/_meta-test#my-test-header"}],"webgl":[{"page":"_meta-test","anchor":"my-test-header","absolute-url":"https://engine.needle.tools/docs/_meta-test#my-test-header"}],"augmented reality":[{"page":"_meta-test","anchor":"my-test-header","absolute-url":"https://engine.needle.tools/docs/_meta-test#my-test-header"}],"hello world":[{"page":"_meta-test","anchor":"ui-canvas","absolute-url":"https://engine.needle.tools/docs/_meta-test#ui-canvas"}],"physics":[{"page":"_meta-test","anchor":"how-physics-works","absolute-url":"https://engine.needle.tools/docs/_meta-test#how-physics-works"}],"codegen":[{"page":"component-compiler","anchor":"automatically-generating-editor-components","absolute-url":"https://engine.needle.tools/docs/component-compiler#automatically-generating-editor-components"},{"page":"component-compiler","anchor":"controlling-component-generation","absolute-url":"https://engine.needle.tools/docs/component-compiler#controlling-component-generation"}],"serialization":[{"page":"scripting","anchor":"serialization-components-in-gltf-files","absolute-url":"https://engine.needle.tools/docs/scripting#serialization-components-in-gltf-files"}]}
\ No newline at end of file
diff --git a/modules.html b/modules.html
new file mode 100644
index 000000000..15a09c008
--- /dev/null
+++ b/modules.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+ Modules and Packages | Needle Engine Documentation
+
+
+
+
+
Projects can be composed of re-usable pieces that we call NpmDef (which stands for Npm Defintion File).
Below you can find links to other repositories that contain Unity packages. These packages can be installed like any Unity package and used in your own projects. They usually contain eihter examples or modules that we use ourselves, but that are not ready to be part of the core Needle Engine.
Custom Timeline Tracks Video Track (sync video playback to a Timeline) CssTrack (control css properties from the Timeline)
Splines (for Unity 2022.1+) Export splines to three.js SplineWalker for controlling camera motion based on a spline Timeline track to control SplineWalker
Google Drive Integration Sketches around drag-drop integration of Google Drive, file picking, app integration
Access to core networking functionality can be obtained by using this.context.connection from a component. The default backend server connects users to rooms. Users in the same room will share state and receive messages from each other.
SyncedRoom โ handles networking connection and connection to a room. This can also be done by code using the networking api accessible from this.context.connection
When sending an object containing a guid field it will be saved in the persistant storage and automatically sent to users that connect later or come back later to the site (e.g. to restore state). To delete state for a specific guid from the backend storage you can use delete-state as the key and provide an object with { guid: "guid_to_delete" }
Subscribe to json events / listen to events in the room using a specific key this.context.connection.beginListen(key:string, callback:(data) => void) Unsubscribe with stopListening
Subscribe to flatbuffer binary events this.context.connection.beginListenBinrary(identifier:string, callback:(data : ByteBuffer) => void) Unsubscribe with stopListenBinary
To automatically network fields in a component you can just decorate a field with a @syncField() decorator (note: you need to have experimentalDecorators: true in your tsconfig.json file for it to work)
Example Code
Automatically network a color field. The following script also changes the color randomly on click
import{ Behaviour, IPointerClickHandler, PointerEventData, Renderer, RoomEvents, delay, serializable, showBalloonMessage, syncField }from"@needle-tools/engine";
+import{ Color }from"three"
+
+exportclassNetworking_ClickToChangeColorextendsBehaviourimplementsIPointerClickHandler{
+
+ // START MARKER network color change syncField
+ /** syncField does automatically send a property value when it changes */
+ @syncField(Networking_ClickToChangeColor.prototype.onColorChanged)
+ @serializable(Color)
+ color!: Color;
+
+ privateonColorChanged(){
+ // syncField will network the color as a number, so we need to convert it back to a Color when we receive it
+ if(typeofthis.color ==="number")
+ this.color =newColor(this.color);
+ this.setColorToMaterials();
+ }
+ // END MARKER network color change syncField
+
+ /** called when the object is clicked and does generate a random color */
+ onPointerClick(_: PointerEventData){
+ const randomColor =newColor(Math.random(), Math.random(), Math.random());
+ this.color = randomColor;
+ }
+
+ onEnable(){
+ this.setColorToMaterials();
+ }
+
+ privatesetColorToMaterials(){
+ const renderer =this.gameObject.getComponent(Renderer);
+ if(renderer){
+ for(let i =0; i < renderer.sharedMaterials.length; i++){
+ // we clone the material so that we don't change the original material
+ // just for demonstration purposes, you can also change the original material
+ const mat = renderer.sharedMaterials[i]?.clone();
+ renderer.sharedMaterials[i]= mat;
+ if(mat &&"color"in mat)
+ mat.color =this.color;
+ }
+ }
+ elseconsole.warn("No renderer found",this.gameObject)
+ }
+
+}
+
Simple networking of a number
import{ Behaviour, syncField }from"@needle-tools/engine"
+
+exportclassAutoFieldSyncextendsBehaviourimplementsIPointerClickHandler{
+
+ // Use `@syncField` to automatically network a field.
+ // You can optionally assign a method or method name to be called when the value changes
+ @syncField("myValueChanged")
+ mySyncedValue?:number=1;
+
+ privatemyValueChanged(){
+ console.log("My value changed",this.mySyncedValue);
+ }
+
+ onPointerClick(){
+ this.mySyncedValue = Math.random();
+ }
+}
+
Needle Engine currently uses its own networking packageopen in new window hosted on npm. By default if not configured differently using the Networking component Needle Engine will connect to a server running on Glitch.
It can be added to your own fastiy or express server running on any server for example by adding the following code on your server after installing the package:
When deploying your app to Glitch, we include a simple networking backend that is great for prototyping and small deployments (~15-20 people at the same time). You can later update to a bigger/better/stronger networking solution if required.
For testing and development purposes it can be desired to run the needle engine networking package on a local server. We have prepared a repository that is setup to host the websocket package and to make that easy for you. Please follow the instructions in the linked repository:
You can also deploy your own networking server on e.g. google cloud. For further instructions please refer to the description found here: Local Networking Repositoryopen in new window
If you want to use a different server for your local development and your hosted development (and the hosted server is not the same as your needle engine deployed website) then you can also enter a absolute URL in the Networking component URL field as well:
If you want to modify the default peerjs options you can call setPeerOptions(opts: PeerjsOptions) with your custom options. This can be used to modify the hosting provider in case where you host your own peerjs server.
Our main project template uses the superfast viteopen in new window bundler. The following shows the structure of the Vite template that we created and ship (altough it is possible to adapt it to your own needs).
Folders
assets/
The asset folder contains exported assets from Unity. E.g. generated gltf files, audio or video files. It is not recommended to manually add files to assets as it will get cleared on building the distribution for the project.
include/
(optional) - If you have custom assets that you need to reference/load add them to the include directory. On build this directory will be copied to the output folder.
src/generated/
The generated javascript code. Do not edit manually!
src/scripts/
Your project specific scripts / components
src/styles/
Stylesheets
*
You can add any new folders here as you please. Make sure to copy them to the output directory when building
Files
index.html
The landing- or homepage of your website
vite.config
The vite configopen in new window. Settings for building the distribution and hosting the development server are made here. It is usually not necessary to edit these settings.
src/main.ts
Included from index.html and importing needle-engine
*
You can add any new files here as you please. Make sure to copy them to the output directory when building (unless they are just being used during development)
Our exporter can be used with other project structures as well, vite is just our go-to frontend bundling tool because of its speed. Feel free to set up your JavaScript project as you like.
This is where project specific/exclusive assets live.
Packages
This is where packages installed for this project live. A package can contain any asset type. The main difference is that it can be added to multiple Unity projects. It therefor is a great method to share code or assets. To learn more about packages see the Unity documentation about packagesopen in new window.
Needle Engine Unity Package
Core/Runtime/Components
Contains all Needle Engine built-in runtime components (See Packages/Needle Engine Exporter in the Unity Project Window)
When creating a new web project in Unity you can choose to create it from a local template (by default we ship a vite based web template). It is also possible to create custom local templates using the Project View context menu in Create/Needle Engine/Project Template.
You can also reference remote templates by entering a repository URL in the ExportInfo project path (this can be saved with your scene for example). When creating a new web project the repository will be either cloned or downloaded (depending on if you have git installed) and searched for a needle.config.json file. If none can be found in the cloned repository the root directory will be used. Examples of remote template projects can be found on github.com/needle-engineopen in new window
If you're planning to only add custom files via NpmDefs and not change the project config (e.g. for a quick fullscreen test), you can prefix the project path with Library. The project will be generated in the Unity Project Library and does not need to be added to source control (the Library folder should be excluded from source control). We call these projects temporary projects. They're great for quickly testing out ideas!
NPM Definition are npm packagesopen in new window tightly integrated into the Unity Editor which makes it easily possible to share scripts with multiple web- or even Unity projects.
C# component stubs for typescript files will also be automatically generated for scripts inside npmdef packages.
To create a NPM Definition right click in the Unity Project browser and select Create/NPM Definition. You can install a NPM Definition package to your runtime project by e.g. selecting your Export Info component and adding it to the dependencies list (internally this will just add the underlying npm package to your package.json).
Don't forget to install the newly added package by e.g. clicking Install on the ExportInfo component and also restart the server if it is already running
To edit the code inside a NPM Definition package just double click the asset NPM Definition asset in your project browser and it will open the vscode workspace that comes with each npmdef.
The needle.config.json is used to provide configuration for the Needle Editor integrations and for the Needle Engine build pipeline plugins.
Paths
buildDirectory
This is where the built project files are being copied to
assetsDirectory
This is where the Editor integration assets will be copied to or created at (e.g. the .glb files exported from Unity or Blender)
scriptsDirectory
This is the directory the Editor integration is watching for code changes to re-generate components
codegenDirectory
This is where the Editor integration is outputting generated files to.
baseUrl
Required for e.g. next.js or SvelteKit integration. When baseUrl is set, relative paths for codegen and inside files are using baseUrl, not assetsDirectory. This is useful in cases where the assetDirectory does not match the server url. For example, the path on disk could be "assetsDirectory": "public/assets", but the framework serves files from "baseUrl": "assets".
Tools
build : { copy: ["myFileOrDirectory"] }
Array of string paths for copying additional files or folders to the buildDirectory. These can either be absolute or relative.
# Example with different baseUrl (e.g. SvelteKit, Next.js)
Files are exported to static/assets but the framework serves them from /assets. In this case, the baseUrl needs to be set to assets so that relative paths in files are correct.
The <needle-engine> web-component comes with a nice collection of built-in attributes that can be used to modify the look and feel of the loaded scene without the need to add or edit the three.js scene directly. The table below shows a list of the most important ones:
Attribute
Description
Loading
src
Path to one or multiple glTF or glb files. Supported types are string, string[] or a stringified array (, separated)
optional, URL to a environment image (environment light)
Interaction
autoplay
add or set to true to auto play animations e.g. <needle-engine autoplay
camera-controls
add or set to true to automatically add OrbitControls if no camera controls are found in the scene
auto-rotate
add to enable auto-rotate (only used with camera-controls)
Events
loadstart
Name of the function to call when loading starts. Note that the arguments are (ctx:Context, evt:Event). You can call evt.preventDefault() to hide the default loading overlay
progress
Name of the function to call when loading updates. onProgress(ctx:Context, evt: {detail: {context:Context, name:string, index:number, count:number, totalProgress01:number}) { ... }
loadfinished
Name of the function to call when loading finishes
Loading Display
Available options to change how the Needle Engine loading display looks. Use ?debugloadingrendering for easier editing
loading-style
Options are light or dark
loading-background-color
PRO โ Change the loading background color (e.g. =#dd5500)
loading-text-color
PRO โ Change the loading text color
loading-logo-src
PRO โ Change the loading logo image
primary-color
PRO โ Change the primary loading color
secondary-color
PRO โ Change the secondary loading color
hide-loading-overlay
PRO โ Do not show the loading overlay, added in Needle Engine > 3.17.1
Internal
hash
Used internally, is appended to the files being loaded to force an update (e.g. when the browser has cached a glb file). Should not be edited manually.
You can easily modify how Needle Engine looks by setting the appropriate attributes on the <needle-engine> web component. Please see the table above for details.
The following table contains available Typescript decorators that Needle Engine provides.
You can think of them as Attributes on steroids (if you are familiar with C#) - they can be added to classes, fields or methods in Typescript to provide additional functionality.
Field & Property Decorators
@serializable()
Add to exposed / serialized fields. Is used when loading glTF files that have been exported with components from Unity or Blender.
@syncField()
Add to a field to network the value when it changes. You can pass in a method to be called when the field changes
@validate()
Add to receive callbacks in the component event method onValidate whenever the value changes. This behaves similar to Unity's onValidate.
Method Decorators
@prefix(<type>) (experimental)
Can be used to easily inject custom code into other components. Optionally return false to prevent the original method from being executed. See the example below
Class Decorators
@registerType
No argument. Can be added to a custom component class to be registered to the Needle Engine types and to enable hot reloading support.
exportclassButtonObjectextendsBehaviour{
+ // you can omit the type if it's a primitive
+ // e.g. Number, String or Bool
+ @serializable()
+ myNumber:number=42;
+
+ // otherwise add the concrete type that you want to serialize to
+ @serializable(EventList)
+ onClick?: EventList;
+
+ @serializable(SomeComponentType)
+ myComponent: SomeComponentType;
+
+ // Note that for arrays you still add the concrete type (not the array)
+ @serializable(Object3D)
+ myObjects: Object3D[];
+}
+
import{ Camera }from"@needle-tools/engine";
+classYourClass{
+ @prefix(Camera)// < this is type that has the method you want to change
+ awake(){// < this is the method name you want to change
+
+ // this is now called before the Camera.awake method runs
+ // NOTE: `this` does now refer to the Camera instance and NOT `YourClass` anymore. This allows you to access internal state of the component as well
+ console.log("Hello camera:",this)
+ // optionally return false if you want to prevent the default behaviour
+ }
+}
+
Below you will find a few basic scripts as a quick reference. We also offer a lot of sample scenes and complete projects that you can download and use as a starting point:
import{ Behaviour, serializable }from"@needle-tools/engine";
+import{ Object3D }from"three"
+
+exportclassMyClassextendsBehaviour{
+ // this will be a "Transform" field in Unity
+ @serializable(Object3D)
+ myObjectReference: Object3D |null=null;
+
+ // this will be a "Transform" array field in Unity
+ // Note that the @serializable decorator contains the array content type! (Object3D and not Object3D[])
+ @serializable(Object3D)
+ myObjectReferenceList: Object3D[]|null=null;
+}
+
# Reference and load an asset from Unity (Prefab or SceneAsset)
import{ Behaviour, serializable, AssetReference }from"@needle-tools/engine";
+
+exportclassMyClassextendsBehaviour{
+
+ // if you export a prefab or scene as a reference from Unity you'll get a path to that asset
+ // which you can de-serialize to AssetReference for convenient loading
+ @serializable(AssetReference)
+ myPrefab?: AssetReference;
+
+ asyncstart(){
+ // directly instantiate
+ const myInstance =awaitthis.myPrefab?.instantiate();
+
+ // you can also just load and instantiate later
+ // const myInstance = await this.myPrefab.loadAssetAsync();
+ // this.gameObject.add(myInstance)
+ // this is useful if you know that you want to load this asset only once because it will not create a copy
+ // since ``instantiate()`` does create a copy of the asset after loading it
+ }
+}
+
import{ Behaviour, serializable, AssetReference }from"@needle-tools/engine";
+
+exportclassLoadingScenesextendsBehaviour{
+ // tell the component compiler that we want to reference an array of SceneAssets
+ // @type UnityEditor.SceneAsset[]
+ @serializable(AssetReference)
+ myScenes?: AssetReference[];
+
+ asyncawake(){
+ if(!this.myScenes){
+ return;
+ }
+ for(const scene ofthis.myScenes){
+ // check if it is assigned in unity
+ if(!scene)continue;
+ // load the scene once
+ const myScene =await scene.loadAssetAsync();
+ // add it to the threejs scene
+ this.gameObject.add(myScene);
+
+ // of course you can always just load one at a time
+ // and remove it from the scene when you want
+ // myScene.removeFromParent();
+ // this is the same as scene.asset.removeFromParent()
+ }
+ }
+
+ onDestroy():void{
+ if(!this.myScenes)return;
+ for(const scene ofthis.myScenes){
+ scene?.unload();
+ }
+ }
+}
+
Add this script to any object in your scene that you want to be clickable. Make sure to also have an ObjectRaycaster component in the parent hierarchy of that object.
test
import{ Behaviour, IPointerClickHandler, PointerEventData, showBalloonMessage }from"@needle-tools/engine";
+
+exportclassClickExampleextendsBehaviourimplementsIPointerClickHandler{
+
+ // Make sure to have an ObjectRaycaster component in the parent hierarchy
+ onPointerClick(_args: PointerEventData){
+ showBalloonMessage("Clicked "+this.name);
+ }
+}
+
Add this script to any object in your scene that you want to be clickable. Make sure to also have an ObjectRaycaster component in the parent hierarchy of that object. The component will send the received click to all connected clients and will raise an event that you can then react to in your app. If you are using Unity or Blender you can simply assign functions to call to the onClick event to e.g. play an animation or hide objects.
EventList events are also invoked on the component level. This means you can also subscribe to the event declared above using myComponent.addEventListener("my-event", evt => {...}) as well. This is an experimental feature: please provide feedback in our discordopen in new window
This is useful for when you want to expose an event to Unity or Blender with some custom arguments (like a string)
import{ Behaviour, serializable, EventList }from"@needle-tools/engine";
+import{ Object3D }from"three";
+
+/*
+Make sure to have a c# file in your project with the following content:
+
+using UnityEngine;
+using UnityEngine.Events;
+
+[System.Serializable]
+public class MyCustomUnityEvent : UnityEvent<string>
+{
+}
+
+Unity documentation about custom events:
+https://docs.unity3d.com/ScriptReference/Events.UnityEvent_2.html
+
+*/
+
+// Documentation โ https://docs.needle.tools/scripting
+
+exportclassCustomEventCallerextendsBehaviour{
+
+ // The next line is not just a comment, it defines
+ // a specific type for the component generator to use.
+
+ //@type MyCustomUnityEvent
+ @serializable(EventList)
+ myEvent!: EventList;
+
+ // just for testing - could be when a button is clicked, etc.
+ start(){
+ this.myEvent.invoke("Hello");
+ }
+}
+
+exportclassCustomEventReceiverextendsBehaviour{
+
+ logStringAndObject(str:string){
+ console.log("From Event: ", str);
+ }
+}
+
You can nest objects and their data. With properly matching @serializable(SomeType) decorators, the data will be serialized and deserialized into the correct types automatically.
Without the correct type decorators, you will still get the data, but just as a plain object. This is useful when you're porting components, as you'll have access to all data and can add types as required.
Keep in mind that you still have access to all web apis and npmopen in new window packages! That's the beauty of Needle Engine if we're allowed to say this here ๐
To use the effect add it to the same object as your Volume component.
Here is an example that wraps the Outline postprocessing effectopen in new window. You can expose variables and settings as usual as any effect is also just a component in your three.js scene.
import{ EffectProviderResult, PostProcessingEffect, registerCustomEffectType, serializable }from"@needle-tools/engine";
+import{ OutlineEffect }from"postprocessing";
+import{ Object3D }from"three";
+
+exportclassOutlinePostEffectextendsPostProcessingEffect{
+
+ // the outline effect takes a list of objects to outline
+ @serializable(Object3D)
+ selection!: Object3D[];
+
+ // this is just an example method that you could call to update the outline effect selection
+ updateSelection(){
+ if(this._outlineEffect){
+ this._outlineEffect.selection.clear();
+ for(const obj ofthis.selection){
+ this._outlineEffect.selection.add(obj);
+ }
+ }
+ }
+
+
+ // a unique name is required for custom effects
+ gettypeName():string{
+ return"Outline";
+ }
+
+ private _outlineEffect:void|undefined| OutlineEffect;
+
+ // method that creates the effect once
+ onCreateEffect(): EffectProviderResult |undefined{
+
+ const outlineEffect =newOutlineEffect(this.context.scene,this.context.mainCamera!);
+ this._outlineEffect = outlineEffect;
+ outlineEffect.edgeStrength =10;
+ outlineEffect.visibleEdgeColor.set(0xff0000);
+ for(const obj ofthis.selection){
+ outlineEffect.selection.add(obj);
+ }
+
+ return outlineEffect;
+ }
+}
+// You need to register your effect type with the engine
+registerCustomEffectType("Outline", OutlinePostEffect);
+
This is an example how you could create your own audio component. For most usecases however you can use the core AudioSource component and don't have to write code.
import{ AudioSource, Behaviour, serializable }from"@needle-tools/engine";
+
+// declaring AudioClip type is for codegen to produce the correct input field (for e.g. Unity or Blender)
+declaretypeAudioClip=string;
+
+exportclassMy2DAudioextendsBehaviour{
+
+ // The clip contains a string pointing to the audio file - by default it's relative to the GLB that contains the component
+ // by adding the URL decorator the clip string will be resolved relative to your project root and can be loaded
+ @serializable(URL)
+ clip?: AudioClip;
+
+ awake(){
+ // creating a new audio element and playing it
+ const audioElement =newAudio(this.clip);
+ audioElement.loop =true;
+ // on the web we have to wait for the user to interact with the page before we can play audio
+ AudioSource.registerWaitForAllowAudio(()=>{
+ audioElement.play();
+ })
+ }
+}
+
Use the FileReference type to load external files (e.g. a json file)
import{ Behaviour, FileReference, ImageReference, serializable }from"@needle-tools/engine";
+
+exportclassFileReferenceExampleextendsBehaviour{
+
+ // A FileReference can be used to load and assign arbitrary data in the editor. You can use it to load images, audio, text files... FileReference types will not be saved inside as part of the GLB (the GLB will only contain a relative URL to the file)
+ @serializable(FileReference)
+ myFile?: FileReference;
+ // Tip: if you want to export and load an image (that is not part of your GLB) if you intent to add it to your HTML content for example you can use the ImageReference type instead of FileReference. It will be loaded as an image and you can use it as a source for an <img> tag.
+
+ asyncstart(){
+ console.log("This is my file: ",this.myFile);
+ // load the file
+ const data =awaitthis.myFile?.loadRaw();
+ if(!data){
+ console.error("Failed loading my file...");
+ return;
+ }
+ console.log("Loaded my file. These are the bytes:",await data.arrayBuffer());
+ }
+}
+
import{ Behaviour, EventList, serializable, serializeable }from"@needle-tools/engine";
+
+exportclassHTMLButtonClickextendsBehaviour{
+
+ /** Enter a button query (e.g. button.some-button if you're interested in a button with the class 'some-button')
+ * Or you can also use an id (e.g. #some-button if you're interested in a button with the id 'some-button')
+ * Or you can also use a tag (e.g. button if you're interested in any button
+ */
+ @serializeable()
+ htmlSelector:string="button.some-button";
+
+ /** This is the event to be invoked when the html element is clicked. In Unity or Blender you can assign methods to be called in the Editor */
+ @serializable(EventList)
+ onClick: EventList =newEventList();
+
+ private element?: HTMLButtonElement;
+
+ onEnable(){
+ // Get the element from the DOM
+ this.element = document.querySelector(this.htmlSelector)as HTMLButtonElement;
+ if(this.element){
+ this.element.addEventListener('click',this.onClicked);
+ }
+ elseconsole.warn(`Could not find element with selector \"${this.htmlSelector}\"`);
+ }
+
+ onDisable(){
+ if(this.element){
+ this.element.removeEventListener('click',this.onClicked);
+ }
+ }
+
+ privateonClicked=()=>{
+ this.onClick.invoke();
+ }
+}
+
# Use mediapipe package to control the 3D scene with hands
Make sure to install the mediapipe package. Visit the github link below to see the complete project setup. Try it live hereopen in new window - requires a webcam/camera
Runtime code for Needle Engine is written in TypeScriptopen in new window (recommended) or JavaScriptopen in new window. We automatically generate C# stub components out of that, which you can add to GameObjects in the editor. The C# components and their data are recreated by the runtime as JavaScript components with the same data and attached to three.js objects.
Both custom components as well as built-in Unity components can be mapped to JavaScript components in this way. For example, mappings for many built-in components related to animation, rendering or physics are already included in Needle Engine.
If you want to code-along with the following examples without having to install anything you just click the following link:
Our web runtime engine adopts a component model similar to Unity and thus provides a lot of functionality that will feel familiar. Components attached to three's Object3D objects have lifecycle methods like awake, start, onEnable, onDisable, update and lateUpdate that you can implement. You can also use Coroutines.
Often, interactive scenes can be realized using Events in Unity and calling methods on built-in components. A typical example is playing an animation on button click - you create a button, add a Click event in the inspector, and have that call Animator.SetTrigger or similar to play a specific animation.
Needle Engine translates Unity Events into JavaScript method calls, which makes this a very fast and flexible workflow - set up your events as usual and when they're called they'll work the same as in Unity.
An example of a Button Click Event that is working out-of-the-box in Needle Engine โ no code needed.
Scripts are written in TypeScript (recommended) or JavaScript. There are two ways to add custom scripts to your project:
Simply add a file with an .ts or .js extension inside src/scripts/ in your generated project directory, for example src/scripts/MyFirstScript.ts
Unity specific: Organize your code into NPM Definition Files (npm packages). These help you to modularize and re-use code between projects and if you are familiar with web development they are in fact regular npm packages that are installed locally. In Unity you can create NpmDef files via Create > NPM Definition and then add TypeScript files by right-clicking an NpmDef file and selecting Create > TypeScript. Please see this chapter for more information.
In both approaches, source directories are watched for changes and C# stub components or Blender panels are regenerated whenever a change is detected. Changes to the source files also result in a hot reload of the running website โ you don't have to wait for Unity to recompile the C# components. This makes iterating on code pretty much instant.
You can even have multiple component types inside one file (e.g. you can declare export class MyComponent1 and export class MyOtherComponent in the same Typescript file).
If you are new to writing Javascript or Typescript we recommend reading the Typescript Essentials Guide guide first before continuing with this guide.
Example: Creating a Component that rotates an object
Create a component that rotates an object Create src/scripts/Rotate.ts and add the following code:
import{ Behaviour, serializable }from"@needle-tools/engine";
+
+exportclassRotateextendsBehaviour
+{
+ @serializable()
+ speed :number=1;
+
+ start(){
+ // logging this is useful for debugging in the browser.
+ // You can open the developer console (F12) to see what data your component contains
+ console.log(this);
+ }
+
+ // update will be called every frame
+ update(){
+ this.gameObject.rotateY(this.context.time.deltaTime *this.speed);
+ }
+}
+
Now inside Unity a new script called Rotate.cs will be automatically generated. Add the new Unity component to a Cube that is exported as part of a glTF file (it needs a GltfObject component in its parent) and save the scene. The cube is now rotating inside the browser. Open the chrome developer console by F12 to inspect the log from the Rotate.start method. This is a helpful practice to learn and debug what fields are exported and currently assigned. In general all public and serializable fields and all public properties are exported.
Now add a new field public float speed = 5 to your Unity component and save it. The Rotate component inspector now shows a speed field that you can edit. Save the scene (or click the Build button) and note that the javascript component now has the exported speed value assigned.
While generated C# components use the type name to produce stable GUIDs, we recommend checking in generated components in version control as a good practice.
Components are added to three.js Object3Ds. This is similar to how Components in Unity are added to GameObjects. Therefore when we want to access a three.js Object3D, we can access it as this.gameObject which returns the Object3D that the component is attached to.
Note: Setting visible to false on a Object3D will act like SetActive(false) in Unity - meaning it will also disable all the current components on this object and its children. Update events for inactive components are not being called until visible is set to true again. If you want to hide an object without affecting components you can just disable the Needle Engine Renderer component.
Note that lifecycle methods are only being called when they are declared. So only declare update lifecycle methods when they are actually necessary, otherwise it may hurt performance if you have many components with update loops that do nothing.
Method name
Description
awake()
First method being called when a new component is created
onEnable()
Called when a component is enabled (e.g. when enabled changes from false to true)
onDisable()
Called when a component is disabled (e.g. when enabled changes from true to false)
onDestroy()
called when the Object3D or component is being destroyed
start()
Called on the start of the first frame after the component was created
Optionally implement if you only want to receive XR callbacks for specific XR modes like immersive-vr or immersive-ar. Return true to notify the system that you want callbacks for the passed in mode
Callback when a controller is connected/added while in a XR session OR when the component joins a running XR session that has already connected controllers OR when the component becomes active during a running XR session that has already connected controllers
import{ Behaviour, FrameEvent }from"@needle-tools/engine";
+
+exportclassRotateextendsBehaviour{
+
+ start(){
+ // the second argument is optional and allows you to specifiy
+ // when it should be called in the current frame loop
+ // coroutine events are called after regular component events of the same name
+ // for example: Update coroutine events are called after component.update() functions
+ this.startCoroutine(this.rotate(), FrameEvent.Update);
+ }
+
+ // this method is called every frame until the component is disabled
+ *rotate(){
+ // keep looping forever
+ while(true){
+ yield;
+ }
+ }
+}
+
To stop a coroutine, either exit the routine by returning from it, or cache the return value of startCoroutine and call this.stopCoroutine(<...>). All Coroutines are stopped at onDisable / when disabling a component.
Needle Engine also exposes a few lifecycle hooks that you can use to hook into the update loop without having to write a full component. Those hooks can be inserted at any point in your web application (for example in toplevel scope or in a svelte component)
Method name
Description
onInitialized()
Called when a new context is initialized (before the first frame)
onStart()
Called directly after components start at the beginning of a frame (once per context)
onUpdate()
Called directly after components update
onBeforeRender()
called before calling render
For example:
// this can be put into e.g. main.ts or a svelte component (similar to onMount)
+import{ onUpdate }from"@needle-tools/engine"
+onUpdate((context:Context)=>{
+ // do something... e.g. access the scene via context.scene
+}
+
To access other components, use the static methods on GameObject or this.gameObject methods. For example, to access a Renderer component in the parent use GameObject.getComponentInParent(this.gameObject, Renderer) or this.gameObject.getComponentInParent(Renderer).
creates a new instance of this object including new instances of all its components
GameObject.destroy(Object3D | Component)
destroy a component or Object3D (and its components)
GameObject.addNewComponent(Object3D, Type)
adds (and creates) a new component for a type to the provided object. Note that awake and onEnable is already called when the component is returned
GameObject.addComponent(Object3D, Component)
moves a component instance to the provided object. It is useful if you already have an instance e.g. when you create a component with e.g. new MyComponent() and then attach it to a object
GameObject.removeComponent(Component)
removes a component from a gameObject
GameObject.getComponent(Object3D, Type)
returns the first component matching a type on the provided object.
GameObject.getComponents(Object3D, Type)
returns all components matching a type on the provided object.
GameObject.getComponentInChildren
same as getComponent but also searches in child objects.
GameObject.getComponentsInChildren
same as getComponents but also searches in child objects.
GameObject.getComponentInParent
same as getComponent but also searches in parent objects.
GameObject.getComponentsInParent
same as getComponents but also searches in parent objects.
The context refers to the runtime inside a web componentopen in new window. The three.js scene lives inside a custom HTML component called <needle-engine> (see the index.html in your project). You can access the <needle-engine> web component using this.context.domElement.
This architecture allows for potentially having multiple needle WebGL scenes on the same webpage, that can either run on their own or communicate between each other as parts of your webpage.
To access the current scene from a component you use this.scene which is equivalent to this.context.scene, this gives you the root three.js scene object.
To traverse the hierarchy from a component you can either iterate over the children of an object with a for loop:
for(let i =0; i <this.gameObject.children; i++)
+ const ch =this.gameObject.children[i];
+
Another option that is quite useful when you just want to iterate objects being renderable you can query all renderer components and iterate over them like so:
You can also subscribe to events in the InputEvents enum like so:
import{ Behaviour, InputEvents }from"@needle-tools/engine";
+
+exportclassMyScriptextendsBehaviour
+{
+ onEnable(){
+ this.context.input.addEventListener(InputEvents.PointerDown,this.onPointerDown);
+ }
+ onDisable(){
+ // it is recommended to also unsubscribe from events when your component becomes inactive
+ this.context.input.removeEventListener(InputEvents.PointerDown,this.onPointerDown);
+ }
+
+ privateonPointerDown=(evt)=>{console.log(evt);}
+}
+
If you want to handle inputs yourself you can also subscribe to all events the browser providesopen in new window (there are a ton). For example to subscribe to the browsers click event you can write:
Note that in this case you have to handle all cases yourself. For example you may need to use different events if your user is visiting your website on desktop vs mobile vs a VR device. These cases are automatically handled by the Needle Engine input events (e.g. PointerDown is raised both for mouse down, touch down and in case of VR on controller button down).
Use this.context.physics.raycast() to perform a raycast and get a list of intersections. If you dont pass in any options the raycast is performed from the mouse position (or first touch position) in screenspace using the currently active mainCamera. You can also pass in a RaycastOptions object that has various settings like maxDistance, the camera to be used or the layers to be tested against.
Note that the calls above are by default raycasting against visible scene objects. That is different to Unity where you always need colliders to hit objects. The default three.js solution has both pros and cons where one major con is that it can perform quite slow depending on your scene geometry. It may be especially slow when raycasting against skinned meshes. It is therefor recommended to usually set objects with SkinnedMeshRenderers in Unity to the Ignore Raycast layer which will then be ignored by default by Needle Engine as well.
Another option is to use the physics raycast methods which will only return hits with colliders in the scene.
const hit =this.context.physics.engine?.raycast();
+
Networking methods can be accessed via this.context.connection. Please refer to the networking docs for further information.
# Accessing Needle Engine and components from anywhere
It is possible to access all the functionality described above using regular JavaScript code that is not inside components and lives somewhere else. All the components and functionality of the needle runtime is accessible via the global Needle namespace (you can write console.log(Needle) to get an overview)
You can find components using Needle.findObjectOfType(Needle.AudioSource) for example. It is recommended to cache those references, as searching the whole scene repeatedly is expensive. See the list for finding adding and removing components above.
For getting callbacks for the initial scene load see the following example:
You can also subscribe to the globale NeedleEngine (sometimes also referred to as ContextRegistry) to receive a callback when a Needle Engine context has been created or to access all available contexts:
Another option is using the onInitialized(ctx => {})lifecycle hook
You can also access all available contexts via NeedleEngine.Registered which returns the internal array. (Note that this array should not be modified but can be used to iterate all active contexts to modify settings, e.g. set all contexts to context.isPaused = true)
Below you find a list of available events on the static NeedleEngine type. You can subscribe to those events via NeedleEngine.registerCallback(ContextEvent.ContextCreated, (args) => {})
ContextEvent options
ContextEvent.ContextRegistered
Called when the context is registered to the registry.
ContextEvent.ContextCreationStart
Called before the first glb is loaded and can be used to initialize the physics engine. Can return a promise
ContextEvent.ContextCreated
Called when the context has been created before the first frame
ContextEvent.ContextDestroyed
Called when the context has been destroyed
ContextEvent.MissingCamera
Called when the context could not find a camera, currently only called during creation
ContextEvent.ContextClearing
Called when the context is being cleared: all objects in the scene are being destroyed and internal state is reset
The static Gizmos class can be used to draw lines, shapes and text which is mostly useful for debugging. All gizmos function have multiple options for e.g. colors or for how long they should be displayed in the scene. Internally they are cached and re-used.
Gizmos
Gizmos.DrawLabel
Draws a label with a background optionally. It can be attached to an object. Returns a Label handle which can be used to update the text.
Gizmos.DrawRay
Takes an origin and direction in worldspace to draw an infinite ray line
Gizmos.DrawDirection
Takes a origin and direction to draw a direction in worldspace
To embed components and recreate components with their correct types in glTF, we also need to save non-primitive types (everything that is not a Number, Boolean or String). You can do so is adding a @serializable(<type>) decorator above your field or property.
Example:
import{ Behaviour, serializable }from"@needle-tools/engine";
+import{ Object3D }from"three"
+
+exportclassMyClassextendsBehaviour{
+ // this will be a "Transform" field in Unity
+ @serializable(Object3D)
+ myObjectReference: Object3D |null=null;
+
+ // this will be a "Transform" array field in Unity
+ // Note that the @serializable decorator contains the array content type! (Object3D and not Object3D[])
+ @serializable(Object3D)
+ myObjectReferenceList: Object3D[]|null=null;
+}
+
To serialize from and to custom formats, it is possible to extend from the TypeSerializer class and create an instance. Use super() in the constructor to register supported types.
Note: In addition to matching fields, matching properties will also be exported when they match to fields in the typescript file.
These exported gltf files will be serialized as plain string URIs. To simplify loading these from TypeScript components, we added the concept of AssetReference types. They can be loaded at runtime and thus allow to defer loading parts of your app or loading external content.
Example:
import{ Behaviour, serializable, AssetReference }from"@needle-tools/engine";
+
+exportclassMyClassextendsBehaviour{
+
+ // if you export a prefab or scene as a reference from Unity you'll get a path to that asset
+ // which you can de-serialize to AssetReference for convenient loading
+ @serializable(AssetReference)
+ myPrefab?: AssetReference;
+
+ asyncstart(){
+ // directly instantiate
+ const myInstance =awaitthis.myPrefab?.instantiate();
+
+ // you can also just load and instantiate later
+ // const myInstance = await this.myPrefab.loadAssetAsync();
+ // this.gameObject.add(myInstance)
+ // this is useful if you know that you want to load this asset only once because it will not create a copy
+ // since ``instantiate()`` does create a copy of the asset after loading it
+ }
+}
+
AssetReferences are cached by URI, so if you reference the same exported glTF/Prefab in multiple components/scripts it will only be loaded once and then re-used.
Build your own castle! Drag 3D models from the various palettes onto the stage, and create your very own world. Works on Desktop, Mobile, VR, AR, all right in your browser. Interactions are currently optimized for VR; placement on screens is a bit harder but possible. Have fun, no matter which device you're on!
Invite your friends! Click Create Room to be put into a live, multi-user space โ just copy the URL, send it to a friend and they join you automatically. There's currently a max limit for "users online at the same time" - if you don't get into a room, please try later.
This page was authored in Unity and exported to three.js using tools and technologies by ๐ต needle.
There are a lot of open technologies involved: 3D models are in glTF format, the render engine is three.js, VR and AR are using WebXR. The networking server runs on Glitch, and audio is sent over WebRTC using PeerJS.
Hello, my name is Kryลกtof and i did a research project about Needle. At our companyopen in new window, we wanted to determine how Needle can help us in our workflow. We have one local client which focuses on reselling luxury cars. We already delivered a mobile app and VR experience using Unity. We have around 30 unique cars ready in the engine. We plan to expand the client's website with visually pleasing digital clones with more configuration options. Needle could achieve a perfect 1:1 conversion between unity and web visuals. It would be a massive benefit to our workflow. So that's what sparked our research.
I'm not very well experienced with javascript, typescript or three.js, so my point of view is as a semi-experienced Unity developer trying out the simplest way how to create a web experience. For those who would suggest Unity WebGL, that sadly doesn't work and isn't flexible on mobile browsers. Needle is ๐
Our lighting model is based on reflection probes in unity. We do not need any directional or point lights, only ambient lighting.
We're using this skybox:
Which looks like this on the paint job:
Then to add a slight detail, i've added 2 directional lights with an insignificant intensity (0.04) to create specular highlights. So before it looked like this:
But with the added directional lights it added a better dynamic. The effect could be deepened with higher intensity:
The black background isn't very pretty. So to differentiate between visual and lighting skyboxes i've added an inverse sphere which wraps the whole map.
Regarding the gradient goes from a slight gray to a white color..
This effect could be easily made with just a proper UV mapping and a single pixel high texture which would define the gradient.
I've made an unlit shader in the shader graph:
I've noticed a color banding issue, so i've tried to implement dithering. Frankly, it didn't help the artefacts but i bet there's a simple solution to that issue. So the upper part of the shader does sample the gradient based on the Y axis in object space. And the lower part tries to negate the color banding.
By using shaders it's simpler to use and iterate the gradiant. By using Needle's Shadergraph markdown asset, it's even simpler! ๐ต
The user now sees a car driving in deep nothingness, the color doesn't resemble anything and the experience is dull. We want to ground the model and that's done by adding a grid and then shifting it so it seems the car is moving. This is what we want to achieve:
The shader for the grid was comprised of two parts. A simple tiled texture of the grid that's being multipled by a circular gradient to make the edges fade off.
This tech demo takes it's goal to showcase the car's capabilities.
Let's start by highlighting the wheels.
Adding this shader to a plane will result in a dashed circle which is rotating by a defined speed. Combined with world space UI with a normal Text component this can highlight some interesting capabilities or parameters of the given product.
After showcasing the wheels we want to finish with a broad information about the product. In this case, that would be the car's full name and perhaps some available configurations.
Needle Engine seems to be a very good candidate for us!
There are a few features which we miss.
That would be for example proper support for the Lit Shader Graphs. But nothing stops us to create shaders the three.js way and create simmilar shaders in Unity for our content team to tweak the materials.
Using Needle was a blast! ๐ต
+
+
+
diff --git a/showcase-mercedes/10_WheelsAndGrid.png b/showcase-mercedes/10_WheelsAndGrid.png
new file mode 100644
index 000000000..869772b0b
Binary files /dev/null and b/showcase-mercedes/10_WheelsAndGrid.png differ
diff --git a/showcase-mercedes/11_GridShader.jpg b/showcase-mercedes/11_GridShader.jpg
new file mode 100644
index 000000000..8aa5c8dd7
Binary files /dev/null and b/showcase-mercedes/11_GridShader.jpg differ
diff --git a/showcase-mercedes/12_WheelWithText.png b/showcase-mercedes/12_WheelWithText.png
new file mode 100644
index 000000000..a2973ab3d
Binary files /dev/null and b/showcase-mercedes/12_WheelWithText.png differ
diff --git a/showcase-mercedes/13_WheelShader.jpg b/showcase-mercedes/13_WheelShader.jpg
new file mode 100644
index 000000000..fcf52ad50
Binary files /dev/null and b/showcase-mercedes/13_WheelShader.jpg differ
diff --git a/showcase-mercedes/14_RearUI.jpg b/showcase-mercedes/14_RearUI.jpg
new file mode 100644
index 000000000..35949e338
Binary files /dev/null and b/showcase-mercedes/14_RearUI.jpg differ
diff --git a/showcase-mercedes/1_skybox.png b/showcase-mercedes/1_skybox.png
new file mode 100644
index 000000000..4611373a0
Binary files /dev/null and b/showcase-mercedes/1_skybox.png differ
diff --git a/showcase-mercedes/2_paintjob_simple.jpg b/showcase-mercedes/2_paintjob_simple.jpg
new file mode 100644
index 000000000..e5be417b3
Binary files /dev/null and b/showcase-mercedes/2_paintjob_simple.jpg differ
diff --git a/showcase-mercedes/3_SpecularHighlights_off.jpg b/showcase-mercedes/3_SpecularHighlights_off.jpg
new file mode 100644
index 000000000..bdc2aa805
Binary files /dev/null and b/showcase-mercedes/3_SpecularHighlights_off.jpg differ
diff --git a/showcase-mercedes/4_SpecularHighlights_on.jpg b/showcase-mercedes/4_SpecularHighlights_on.jpg
new file mode 100644
index 000000000..fad273a28
Binary files /dev/null and b/showcase-mercedes/4_SpecularHighlights_on.jpg differ
diff --git a/showcase-mercedes/5_NoBackground.jpg b/showcase-mercedes/5_NoBackground.jpg
new file mode 100644
index 000000000..7f607b9de
Binary files /dev/null and b/showcase-mercedes/5_NoBackground.jpg differ
diff --git a/showcase-mercedes/6_MapBackground.png b/showcase-mercedes/6_MapBackground.png
new file mode 100644
index 000000000..e17bd6117
Binary files /dev/null and b/showcase-mercedes/6_MapBackground.png differ
diff --git a/showcase-mercedes/7_EnvShaderGraph.jpg b/showcase-mercedes/7_EnvShaderGraph.jpg
new file mode 100644
index 000000000..3d91713dd
Binary files /dev/null and b/showcase-mercedes/7_EnvShaderGraph.jpg differ
diff --git a/showcase-mercedes/8_Gradiant.png b/showcase-mercedes/8_Gradiant.png
new file mode 100644
index 000000000..e68c7984e
Binary files /dev/null and b/showcase-mercedes/8_Gradiant.png differ
diff --git a/showcase-mercedes/9_Rotator.png b/showcase-mercedes/9_Rotator.png
new file mode 100644
index 000000000..8ca66499d
Binary files /dev/null and b/showcase-mercedes/9_Rotator.png differ
diff --git a/showcase-monsterhands.html b/showcase-monsterhands.html
new file mode 100644
index 000000000..2255b49de
--- /dev/null
+++ b/showcase-monsterhands.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+ Monster Hands ๐ | Needle Engine Documentation
+
+
+
+
+
a number of components and tools that allow you to set up scenes for Needle Engine from e.g. the Unity Editor.
an exporter that turns scene and component data into glTF.
a web runtime that loads and runs the produced glTF files and their extensions.
The editor extensions currently support the Unity Editor, with some promising experiments for Blender on the horizon (but no ETA). The web runtime uses three.js for rendering, adds a component system on top of the three scene graph and hooks up extension loaders for our custom glTF extensions.
Effectively, this turns the Unity Editor into a full member of a regular web development toolchain โ "just" one more piece that gets added to the regular HTML, JavaScript, CSS and bundling workflow.
Models, textures, animations, lights, cameras and more are stored as glTF 2.0 filesopen in new window in Needle Engine. Custom data is stored in vendor extensions. These cover everything from interactive components to physics, sequencing and lightmaps.
More extensions and custom extensions can be added using the export callbacks of UnityGLTF (not documented yet) and the glTF import extensionsopen in new window of three.js.
Note: Materials using these extensions can be exported from Unity via UnityGLTF's PBRGraph material.
Note: Audio and variants are already supported in Needle Engine through NEEDLE_components and NEEDLE_persistent_assets, but there are some options for more alignment to existing proposals such as KHR_audio and KHR_materials_variants.
For production, we compress glTF assets with glTF-transformopen in new window. Textures use either etc1s, uastc, webp or no compression, depending on texture type. Meshes use draco by default but can be configured to use meshtopt (per glTF file). Custom extensions are passed through in an opaque way.
Needle Engine stores custom data in glTF files through our vendor extensions. These extensions are designed to be flexible and allow relatively arbitrary data to put into them. Notably, no code is stored in these files. Interactive components is restored from the data at runtime. This has some similarities to how AssetBundles function in Unity โ the receiving side of an asset needs to have matching code for components stored in the file.
We're currently not prodiving schemas for these extensions as they are still in development. The JSON snippets below demonstrates extension usage by example and includes notes on architectural choices and what we may change in future releases.
References between pieces of data are currently constructed through a mix of indices into other parts of the glTF file and JSON pointers. We may consolidate these approaches in a future release. We're also storing string-based GUIDs for cases where ordering is otherwise hard to resolve (e.g. two components referencing each other) that we may remove in the future.
This extension contains per-node component data. The component names map to type names on both the JavaScript and C# side. Multiple components with the same name can be added to the same node.
Note: Storing only the component type name means that type names currently need to be unique per project. We're planning to include package names in a future release to loosen this constraint to unique component type names per package instead of globally.
Note: Currently there's no versioning information in the extension (which npm packaage does a component belong to, which version of that package was it exported against). We're planning to include versioning information in a future release.
Note: Currently all components are in the builtin_components array. We might rename this to just components in a future release.
This extension contains additional per-node data related to state, layers, and tags. Layers are used for both rendering and physics, similar to how three.jsopen in new window and Unityopen in new window treat them.
Components in NEEDLE_components can reference data via JSON Pointers. The data in NEEDLE_persistent_assets is often referenced multiple times by different components and is thus stored separately in a root extension. By design, they are always referenced by something else (or have references within each other), and thus do not store type information at all: they're simply pieces of JSON data and components referencing them currently need to know what they expect.
Examples for assets/data stored in here are:
AnimatorControllers, their layers and states
PlayableAssets (timelines), their tracks and embedded clips
SignalAssets
...
Data in persistent_assets can reference other persistent_assets via JSON Pointer, but by design can't reference NEEDLE_components. This is similar to the separation beween "Scene data" and "AssetDatabase content" in Unity.
This extension builds upon the archived KHR_techniques_webglopen in new window extension and extends it in a few crucial places. While the original extension was specified against WebGL 1.0, we're using it with WebGL 2.0 here and have added a number of uniform types.
While Unity's compilation process from C# to IL to C++ (via IL2CPP) to WASM (via emscripten) is ingenious, it's also relatively slow. Building even a simple project to WASM takes many minutes, and that process is pretty much repeated on every code change. Some of it can be avoided through clever caching and ensuring that dev builds don't try to strip as much code, but it still stays slow.
We do have a prototype for some WASM translation, but it's far from complete and the iteration speed stays slow, so we are not actively investigating this path right now.
When looking into modern web workflows, we found that code reload times during development are neglectible, usually in sub-second ranges. This of course trades some performance (interpretation of JavaScript on the fly instead of compiler optimization at build time) for flexibility, but browsers got really good at getting the most out of JavaScript.
We believe that for iteration and tight testing workflows, it's beneficial to be able to test on device and on the target platform (the browser, in this case) as quickly and as often as possible - which is why we're skipping Unity's entire play mode, effectively always running in the browser.
Note: A really nice side effect is avoiding the entire slow "domain reload" step that usually costs 15-60 seconds each time you enter Play Mode. You're just "live" in the browser the moment you press Play.
This is the best thing I have seen after cinemachine in unity. Unity should acquire this โ Rinesh Thomas
Unbelievable Unity editor integration by an order of magnitude, and as straightforward as the docs claim. Wow. โ Chris Mahoney
needle.tools is a wonderful showcase of what @NeedleTools contributes to 3D via the web. I just love it. โ Kevin Curry
Thanks to @NeedleTools, seeing quite a bit of this solution for web-based real time 3d tools - export scenes from Unity, where you can leverage the extensive 3d editor ecosystem & content, and then render them in your own web-based engine โ Stella Cannefax
Played with this a bit this morning ๐คฏ๐คฏ pretty magical โ Brit Gardner
This is huge for WebXR and shared, immersive 3D experiences! Thank you so much for putting in the work on this @NeedleTools crew! Hoping @Apple sort out their WebXR situation sooner rather than later. The AR part worked flawlessly on my @SamsungMobile S21. โ Marc Wakefield
Finally checking out @NeedleTools with Unity. Super easy to get something up and running in the cloud using their glitch integration โ Pete Patterson
This is amazing and if you are curious about #WebXR with Unity this will help us get there โ Dilmer Valecillos
I am a long time Unity dev and recently started playing with Needle Tools and I love it! It's a great on ramp for Unity devs who want to learn WebXR and three.js. The runtime engine is awesome and it was pretty easy to create my own custom component โ VRSpatialist
Spent the last 2.5 months building this game, never built a game/never used unity before, but absolutely loving the whole process with needle tools. So rapid! Would love to make a career building AR experiences! โ Matthew Pieri
When using our templates, Needle Engine runs a local development server for you. Simply press play, and a website will open in your default browser, ready for testing on your local device. For testing on other devices in the same network, you may have to install a self-signed certificate (see below).
When using a custom setup / framework, please refer to your framework's documentation on how to run a secure local development server.
TIP
Our local server uses the IP address in your local network (e.g. https://192.168.0.123:3000) instead of localhost:3000. This allows you to quickly view and test your project from mobile devices, VR glasses, and other machines in the same network.
We're using HTTPS instead of the older HTTP, because modern powerful web APIs like WebXR require a secure connection โ the S stands for "secure".
# Setting up a self-signed certificate for development
Different operating systems have different security requirements for local development. Typically, displaying a website will work even with a auto-generated untrusted certificate, but browsers may warn about the missing trust and some features are not available. Here's a summary:
TIP
Installing trusted self-signed certificates on your development devices is recommended for a smooth development experience. Find the steps at the bottom of this page.
Default โ with auto-generated untrusted certificate Displays a certificate warning upon opening the project in a browser.Uses the vite-plugin-basic-sslopen in new window npm package.
We're using websocket connections to communicate between the browser and the local development server. Websockets require a secure connection (HTTPS) in order to work. Depending on the platform, you might need to set up a custom certificate for local development. Android and Windows don't need a custom certificate, but iOS and MacOS do.
OS
Viewing in the browser (with a security warning)
Automatic reloads
Windows
(โ)
โ
Linux
(โ)
โ
Android
(โ)
โ
macOS
(โ)
โ
iOS
(โ Safari and Chrome, after page reload) โ Mozilla XR Viewer
โ
Xcode Simulators
(โ after page reload)
โ
With a self-signed, trusted root certificate No security warning is displayed. You need to install the generated certificate on your device(s). Uses the vite-plugin-mkcertopen in new window npm package.
OS
Viewing in the browser
Automatic reloads
Windows
โ
โ
Linux
โ
โ
Android
โ
โ
macOS
โ
โ
iOS
โ
โ
# Generating a self-signed development certificate
in Unity/Blender, click on "Open Workspace" to open VS Code
check that you're using vite-plugin-mkcert instead of vite-plugin-basic-ssl (the latter doesn't generate a trusted root certificate, which we need) in your vite.config.ts file:
open package.json
remove the line with "@vitejs/plugin-basic-ssl" from devDependencies
open a Terminal in VS Code and run npm install vite-plugin-mkcert --save-dev which will add the latest version
open vite.config.ts and replace import basicSsl from '@vitejs/plugin-basic-ssl' with import mkcert from'vite-plugin-mkcert'
in plugins: [, replace basicSsl(), with mkcert(),
package.json looks like this now:
run npm run start once from VS Code's terminal
on Windows, this will open a new window and guide you through the creation and installation of the certificate
on MacOS, the terminal prompts for your password and then generates and installs the certificate.
if you're getting an error Error: Port 3000 is already in use, please close the server that may still be running from Unity.
the generated certificate will automatically be installed on the machine you generated it on.
you can stop the terminal process again.
from now on, pressing Play in Unity/Blender will use the generated certificate for the local server, and no "security warning" will be shown anymore, since your browser now trusts the local connection.
# Installing the certificate on your development devices
On your development devices, you need to install the generated certificate and allow the OS to trust it. This is different for each OS. For each of them, you'll need the rootCA.pem file that was generated, and send it to the device you want to authenticate.
On Windows: find the certificate in Users/<your-user>/.vite-plugin-mkcert/rootCA.pem On MacOS: find the certificate in Users/<your-user>/.vite-plugin-mkcert/rootCA.pem
Send the device to yourself (e.g. via E-Mail, AirDrop, iCloud, USB, Slack, ...) so that you can access it on your development devices.
Open the file. You'll be prompted to install the certificate.
# Installing the certificate on iOS / iPadOS / VisionOS
Open the file.
You'll be prompted to add the profile to your device. Confirm.
Go to Settings
There will be a new entry "Profile". Select it and allow the profile to be active for this device.
On iOS / iPadOS, you also need to allow "Root Certificate Trust". For this, search for Trust or go to Settings > General > About > Info > Certificate Trust Settings and enable full trust for the root certificate.
TIP
The certificate is automatically installed on the machine you generated it on. For other machines in the local network, follow the steps below to also establish a trusted connection.
# Installing the certificate on another MacOS machine
Open the file. Keychain Access will open and allow you to install the certificate.
You may have to set "Trust" to "Always allow".
# Installing the certificate on another Windows machine
Open certmgr ("Manage user certificates") by typing Windows key + certmgr.
In the left sidebar, select "Trusted Root Certification Authorities".
Right-click on "Certificates" and select "All Tasks > Import".
Select the rootCA.pem file (you may have to change the file type to "all") and follow the instructions.
Make sure to update the <needle-engine src="myScene.glb"> path to an existing glb file or download this sample glbopen in new window and put it in the same folder as the index.html, name it myScene.glb or update the src path.
<!DOCTYPEhtml>
+<htmllang="en">
+
+<head>
+ <metacharset="UTF-8"/>
+ <linkrel="icon"href="favicon.ico">
+ <metaname="viewport"content="width=device-width, user-scalable=no">
+ <title>Made with Needle</title>
+
+ <!-- importmap -->
+ <scripttype="importmap">
+ {
+ "imports":{
+ "three":"https://unpkg.com/three/build/three.module.js",
+ "three/":"https://unpkg.com/three/"
+ }
+ }
+ </script>
+ <!-- parcel require must currently defined somewhere for peerjs -->
+ <script>var parcelRequire;</script>
+
+ <!-- the .light version does not include dependencies like Rapier.js (so no physics) to reduce the bundle size, if your project needs physics then just change it to needle-engine.min.js -->
+ <scripttype="module"src="https://unpkg.com/@needle-tools/engine@3.5.6-alpha/dist/needle-engine.light.min.js"></script>
+
+ <style>
+ body{margin: 0;}
+ needle-engine{
+ position: absolute;
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: grey;
+ }
+ </style>
+
+</head>
+
+<body>
+
+ <!-- load a gltf or glb here as your scene and listen to the finished event to start interacting with it -->
+ <needle-enginesrc="myScene.glb"loadfinished="onLoaded"></needle-engine>
+
+</body>
+
+<script>
+ functiononLoaded(ctx){
+ console.log("Loading a glb file finished ๐");
+ console.log("This is the scene: ", ctx.scene);
+ }
+</script>
+
+</html>
+
We believe the use of 3D on the web will expand considerably in the next years. While today native apps are the norm, more and more content is made available as a web app or PWAopen in new window. New VR and AR devices will extend into the webopen in new window, creating an interesting problem: responsive suddenly not only means "small screen" or "large screen", you're also dealing with spaces, 3D, spatial placement and potentially glasses and controllers!
Add to that a push towards more interactivity and collaboration, and you have an interesting mix of challenges.
At Needle, we believe ideating and creating in this space should be easy. We've set out to speed things up โ creating our own runtime to reach these goals. That's why we're baking the ability to deploy to AR and VR right into our core components, and continually test that new ideas work across platforms.
# Why another platform for 3D on the web? Aren't there enough options already?
There's numerous options, that's true! We found that current systems1 can be roughly sorted into two categories: some have great asset handling, tools, and artist-friendly workflows but output some sort of binary blob, and others are more code-focussed, developer-friendly and allow for great integration into modern web workflows2.
We want to bridge these worlds and combine the best of both worlds: artist-friendly workflows and modern web technologies. Combined with modern formats and a snappy workflow, we believe this will allow many more creators to bring their content to the web. We also saw an opportunity to get AR, VR and collaboration right from the start.
1: Examples include Unity, PlayCanvas, three.js, react-three-fiber, Babylon, A-Frame, Godot, and many more.2: There's more nuance to this than fits into an introductory paragraph! All engines and frameworks have their strengths and weaknesses, and are constantly evolving.
We think the next wave of 3D apps on the web will come with better workflows: everyone should be able to put together a 3D scene, an art gallery, present a product or 3D scan on the web or make simple games. Reaching this goal will require more than just supporting one particular system and exporting to the web from there.
Our goal is to allow people to bring data to the web from their creative tools: be it Unity, Blender, Photoshop, or something else. We're aware that this is a big goal โ but instead of doing everything at once, we want to iterate and get closer to it together.
# Open Standards instead of Proprietary Containers
At the core of Needle Engine stands the glTFopen in new window format and its ability to be extended with custom extensions. The goal is: a single .glb file can contain your entire application's data.
It's worth noting that it's not a goal to ship actual code inside glTF; shipping and running code is the job of modern web runtimes and bundling. We certainly can imagine that abstract representations of logic (e.g. graphs, state machines, and so on) can be standardized to a certain degree and allow for interoperable worlds, but we're not there yet.
From working with Unity for many years we've found that while the engine and editor progress at a great pace, WebGL output has somewhat lacked behind. Integration of Unity players into web-based systems is rather hard, "talking" to the surrounding website requires a number of workarounds, and most of all, iteration times are very slow due to the way that Unity packs all code into WebAssembly via IL2CPP. These technologies are awesome, and result in great runtime performance and a lot of flexibility. But they're so much slower and walled off compared to modern web development workflows that we decided to take matters into our own hands.
Needle Engine builds on three.js. All rendering goes through it, glTF files are loaded via three's extension interfaces, and our component system revolves around three's Object3D and scene graph. We're committed to upstreaming some of our changes and improvements, creating pull requests and reporting issues along the way.
+
+
+
diff --git a/workbox-b41f8fb8.js b/workbox-b41f8fb8.js
new file mode 100644
index 000000000..d47666d90
--- /dev/null
+++ b/workbox-b41f8fb8.js
@@ -0,0 +1 @@
+define(["exports"],(function(t){"use strict";try{self["workbox:core:6.5.4"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:6.5.4"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class i{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class r extends i{constructor(t,e,s){super((({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)}),e,s)}}class o{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",(t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map((e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})})));t.waitUntil(s),t.ports&&t.ports[0]&&s.then((()=>t.ports[0].postMessage(!0)))}}))}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let o=r&&r.handler;const c=t.method;if(!o&&this.i.has(c)&&(o=this.i.get(c)),!o)return;let a;try{a=o.handle({url:s,request:t,event:e,params:i})}catch(t){a=Promise.reject(t)}const h=r&&r.catchHandler;return a instanceof Promise&&(this.o||h)&&(a=a.catch((async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:i})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n}))),a}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const i=this.t.get(s.method)||[];for(const r of i){let i;const o=r.match({url:t,sameOrigin:e,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&0===i.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(i=void 0),{route:r,params:i}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let c;const a=()=>(c||(c=new o,c.addFetchListener(),c.addCacheListener()),c);const h={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},u=t=>[h.prefix,t,h.suffix].filter((t=>t&&t.length>0)).join("-"),l=t=>t||u(h.precache),f=t=>t||u(h.runtime);function w(t,e){const s=e();return t.waitUntil(s),s}try{self["workbox:precaching:6.5.4"]&&_()}catch(t){}function d(t){if(!t)throw new s("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const i=new URL(n,location.href),r=new URL(n,location.href);return i.searchParams.set("__WB_REVISION__",e),{cacheKey:i.href,url:r.href}}class p{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:t,state:e})=>{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class y{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.h.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.h=t}}let g;async function R(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const i=t.clone(),r={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=e?e(r):r,c=function(){if(void 0===g){const t=new Response("");if("body"in t)try{new Response(t.body),g=!0}catch(t){g=!1}g=!1}return g}()?i.body:await i.blob();return new Response(c,o)}function m(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class v{constructor(){this.promise=new Promise(((t,e)=>{this.resolve=t,this.reject=e}))}}const q=new Set;try{self["workbox:strategies:6.5.4"]&&_()}catch(t){}function U(t){return"string"==typeof t?new Request(t):t}class L{constructor(t,e){this.u={},Object.assign(this,e),this.event=e.event,this.l=t,this.p=new v,this.R=[],this.m=[...t.plugins],this.v=new Map;for(const t of this.m)this.v.set(t,{});this.event.waitUntil(this.p.promise)}async fetch(t){const{event:e}=this;let n=U(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const i=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const r=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.l.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:r,response:t});return t}catch(t){throw i&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:i.clone(),request:r.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=U(t);let s;const{cacheName:n,matchOptions:i}=this.l,r=await this.getCacheKey(e,"read"),o=Object.assign(Object.assign({},i),{cacheName:n});s=await caches.match(r,o);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(t,e){const n=U(t);var i;await(i=0,new Promise((t=>setTimeout(t,i))));const r=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(o=r.url,new URL(String(o),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var o;const c=await this.q(e);if(!c)return!1;const{cacheName:a,matchOptions:h}=this.l,u=await self.caches.open(a),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const i=m(e.url,s);if(e.url===i)return t.match(e,n);const r=Object.assign(Object.assign({},n),{ignoreSearch:!0}),o=await t.keys(e,r);for(const e of o)if(i===m(e.url,s))return t.match(e,n)}(u,r.clone(),["__WB_REVISION__"],h):null;try{await u.put(r,l?c.clone():c)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of q)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:a,oldResponse:f,newResponse:c.clone(),request:r,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.u[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=U(await t({mode:e,request:n,event:this.event,params:this.params}));this.u[s]=n}return this.u[s]}hasCallback(t){for(const e of this.l.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.l.plugins)if("function"==typeof e[t]){const s=this.v.get(e),n=n=>{const i=Object.assign(Object.assign({},n),{state:s});return e[t](i)};yield n}}waitUntil(t){return this.R.push(t),t}async doneWaiting(){let t;for(;t=this.R.shift();)await t}destroy(){this.p.resolve(null)}async q(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class b{constructor(t={}){this.cacheName=f(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,i=new L(this,{event:e,request:s,params:n}),r=this.U(i,s,e);return[r,this.L(r,i,s,e)]}async U(t,e,n){let i;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(i=await this._(e,t),!i||"error"===i.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const r of t.iterateCallbacks("handlerDidError"))if(i=await r({error:s,event:n,request:e}),i)break;if(!i)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))i=await s({event:n,request:e,response:i});return i}async L(t,e,s,n){let i,r;try{i=await t}catch(r){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:i}),await e.doneWaiting()}catch(t){t instanceof Error&&(r=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:i,error:r}),e.destroy(),r)throw r}}class C extends b{constructor(t={}){t.cacheName=l(t.cacheName),super(t),this.C=!1!==t.fallbackToNetwork,this.plugins.push(C.copyRedirectedCacheableResponsesPlugin)}async _(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.O(t,e):await this.N(t,e))}async N(t,e){let n;const i=e.params||{};if(!this.C)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=i.integrity,r=t.integrity,o=!r||r===s;n=await e.fetch(new Request(t,{integrity:"no-cors"!==t.mode?r||s:void 0})),s&&o&&"no-cors"!==t.mode&&(this.k(),await e.cachePut(t,n.clone()))}return n}async O(t,e){this.k();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}k(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==C.copyRedirectedCacheableResponsesPlugin&&(n===C.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(C.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}C.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},C.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await R(t):t};class E{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.K=new Map,this.T=new Map,this.W=new Map,this.l=new C({cacheName:l(t),plugins:[...e,new y({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.l}precache(t){this.addToCacheList(t),this.j||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.j=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:i}=d(n),r="string"!=typeof n&&n.revision?"reload":"default";if(this.K.has(i)&&this.K.get(i)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.K.get(i),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.W.has(t)&&this.W.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:i});this.W.set(t,n.integrity)}if(this.K.set(i,t),this.T.set(i,r),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return w(t,(async()=>{const e=new p;this.strategy.plugins.push(e);for(const[e,s]of this.K){const n=this.W.get(s),i=this.T.get(e),r=new Request(e,{integrity:n,cache:i,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:r,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}}))}activate(t){return w(t,(async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.K.values()),n=[];for(const i of e)s.has(i.url)||(await t.delete(i),n.push(i.url));return{deletedURLs:n}}))}getURLsToCacheKeys(){return this.K}getCachedURLs(){return[...this.K.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.K.get(e.href)}getIntegrityForCacheKey(t){return this.W.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}let O;const x=()=>(O||(O=new E),O);class N extends i{constructor(t,e){super((({request:s})=>{const n=t.getURLsToCacheKeys();for(const i of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:i}={}){const r=new URL(t,location.href);r.hash="",yield r.href;const o=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some((t=>t.test(s)))&&t.searchParams.delete(s);return t}(r,e);if(yield o.href,s&&o.pathname.endsWith("/")){const t=new URL(o.href);t.pathname+=s,yield t.href}if(n){const t=new URL(o.href);t.pathname+=".html",yield t.href}if(i){const t=i({url:r});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(i);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}}),t.strategy)}}function k(t){const e=x();!function(t,e,n){let o;if("string"==typeof t){const s=new URL(t,location.href);o=new i((({url:t})=>t.href===s.href),e,n)}else if(t instanceof RegExp)o=new r(t,e,n);else if("function"==typeof t)o=new i(t,e,n);else{if(!(t instanceof i))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});o=t}a().registerRoute(o)}(new N(e,t))}t.precacheAndRoute=function(t,e){!function(t){x().precache(t)}(t),k(e)}}));
diff --git a/xr.html b/xr.html
new file mode 100644
index 000000000..c5d5219fa
--- /dev/null
+++ b/xr.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+ VR & AR (WebXR) | Needle Engine Documentation
+
+
+
+
+
Theoretically all WebXR-capable devices and browsers should be supported. That being said, we've tested the following configurations:
Tested VR Device
Browser
Notes
Meta Quest 1
โ๏ธ Meta Browser
hand tracking, support for sessiongranted1
Meta Quest 2
โ๏ธ Meta Browser
hand tracking, support for sessiongranted1, passthrough (black and white)
Meta Quest Pro
โ๏ธ Meta Browser
hand tracking, support for sessiongranted1, passthrough
Pico Neo 3
โ๏ธ Pico Browser
no hand tracking, inverted controller thumbsticks
Pico Neo 4
โ๏ธ Pico Browser
passthrough, hand tracking2
Oculus Rift 1/2
โ๏ธ Chrome
Hololens 2
โ๏ธ Edge
hand tracking, support for AR and VR (in VR mode, background is rendered as well)
Meta Quest 1/2 via Oculus Link
โ๏ธ Chrome
no hand tracking, known issues with repeated refresh of a WebXR session in Chrome
Looking Glass Portrait
โ๏ธ Chrome
requires shim, see samples
Tested AR Device
Browser
Notes
Android 10+
โ๏ธ Chrome
Android 10+
โ๏ธ Firefox
iOS 15+
โ๏ธ WebXR Viewer
does not fully implement standards, but supported
iOS 15+
(โ๏ธ)3 Safari
No full code support, but Needle Everywhere Actions are supported for creating dynamic, interactive USDZ files.
Hololens 2
โ๏ธ Edge
Hololens 1
โ
no WebXR support
Magic Leap 2
โ๏ธ
Not Tested but Should Workโข๏ธ
Browser
Notes
Magic Leap 1
please let us know if you tried!
1: Requires enabling a browser flag: chrome://flags/#webxr-navigation-permission 2: Requires enabling a toggle in the Developer settings 3: Uses Everywhere Actions or other approaches
AR, VR and networking capabilites in Needle Engine are designed to be modular. You can choose to not support any of them, or add only specific features.
Enable AR and VR Add a WebXR component. Optional: you can set a custom avatar by referencing an Avatar Prefab. By default a very basic DefaultAvatar is assigned.
Enable Teleportation Add a TeleportTarget component to object hierarchies that can be teleported on. To exclude specific objects, set their layer to IgnoreRaycasting.
Enable going into Miniature Mode Pointing onto a surface and pressing on a controller switches between 1:10 scale (miniature mode) and 1:1 scale. We're planning to add a component to have more control over this functionality.
Define the AR Session Root and Scale Add a WebARSessionRoot component to your root object. Here you can define the user scale to shrink (< 1) or enlarge (> 1) the user in relation to the scene when entering AR.
Define whether an object is visible in Browser, AR, VR, First Person, Third Person Add a XR Flag component to the object you want to control. Change options on the dropdown as needed.
Common usecases are
hiding floors when entering AR
hiding Avatar parts in First or Third Person views (e.g. first-person head shouldn't be visible).
Needle Engine supports the sessiongrantedopen in new window state. This allows users to seamlessly traverse between WebXR applications without leaving an immersive session โ they stay in VR or AR.
Currently, this is only supported on Oculus Quest 1 and 2 in the Oculus Browser. On other platforms, users will be kicked out of their current immersive session and have to enter VR again on the new page. Requires enabling a browser flag: chrome://flags/#webxr-navigation-permission
Click on objects to open links Add the OpenURL component that makes it very easy to build connected worlds.
There's a number of experimental components to build more expressive Avatars. At this point we recommended duplicating them to make your own variants, since they might be changed or removed at a later point.
Example Avatar Rig with basic neck model and limb constraints
Random Player Colors As an example for avatar customization, you can add a PlayerColor component to your renderers. This randomized color is synchronized between players.
Eye Rotation AvatarEyeLook_Rotation rotates GameObjects (eyes) to follow other avatars and a random target. This component is synchronized between players.
Eye Blinking AvatarBlink_Simple randomly hides GameObjects (eyes) every few seconds, emulating a blink.
Example Avatar Prefab hierarchy
Offset Constraint OffsetConstraint allows to shift an object in relation to another one in Avatar space. This allows, for example, to have a Body follow the Head but keep rotation levelled. It also allows to construct simple neck models.
Limb Constraint BasicIKConstraint is a very minimalistic constraint that takes two transforms and a hint. This is useful to construct simple arm or leg chains. As rotation is currently not properly implemented, arms and legs may need to be rotationally symmetric to "look right". It's called "Basic" for a reason!
If you want to display different html content whether the client is using a regular browser or using AR or VR, you can just use a set of html classes. This is controlled via HTML element classes. For example, to make content appear on desktop and in AR add a <div class="desktop ar"> ... </div> inside the <needle-engine> tag:
<needle-engine>
+ <divclass="desktop ar"style="pointer-events:none;">
+ <divclass="positioning-container">
+ <p>your content for AR and desktop goes here</p>
+ <pclass="only-in-ar">This will only be visible in AR</p>
+ <div>
+ </div>
+</needle-engine>
+
Content Overlays are implemented using the optional dom-overlay feature which is usually supported on screen-based AR devices (phones, tablets).
Use the .ar-session-active class to show/hide specific content while in AR. The :xr-overlay pseudo classopen in new window shouldn't be used at this point because using it breaks Mozilla's WebXR Viewer.
Augmented Reality experiences on iOS are somewhat limited, due to Apple currently not supporting WebXR on iOS devices.
Needle Engine's Everywhere Actions are designed to fill that gap, bringing automatic interactive capabilities to iOS devices for scenes composed of specific components. They support a subset of the functionality that's available in WebXR, for example spatial audio, image tracking, animations, and more. See the docs for more information.
# Musical Instrument โ WebXR and QuickLook support
Here's an example for a musical instrument that uses Everywhere Actions and thus works in browsers and in AR on iOS devices. It uses spatial audio, animation, and tap interactions.
There's also other options for guiding iOS users to even more capable interactive AR experiences:
Exporting content on-the-fly as USDZ files. These files can be displayed on iOS devices in AR. When exported from scenes with Everywhere Actions the interactivity is the same, more than sufficient for product configurators, narrative experiences and similar. An example is Castle Builderopen in new window where creations (not the live session) can be viewed in AR.
Encryption in Spaceopen in new window uses this approach. Players can collaboratively place text into the scene on their screens and then view the results in AR on iOS. On Android, they can also interact right in WebXR. โ #madewithneedle by Katja Rempel ๐
Guiding users towards WebXR-compatible browsers on iOS. Depending on your target audience, you can guide users on iOS towards for example Mozilla's WebXR Vieweropen in new window to experience AR on iOS.
Using camera access and custom algorithms on iOS devices. One can request camera image access and run custom algorithms to determine device pose. While we currently don't provide built-in components for this, here's a few references to libraries and frameworks that we want to try in the future: