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/ configuration.png b/.preview/ configuration.png
new file mode 100644
index 000000000..2927c5e7a
Binary files /dev/null and b/.preview/ configuration.png differ
diff --git a/.preview/@serializable and other decorators.png b/.preview/@serializable and other decorators.png
new file mode 100644
index 000000000..9a6c94df7
Binary files /dev/null and b/.preview/@serializable and other decorators.png differ
diff --git a/.preview/additional modules.png b/.preview/additional modules.png
new file mode 100644
index 000000000..6fd280ff8
Binary files /dev/null and b/.preview/additional modules.png differ
diff --git a/.preview/automatic component generation.png b/.preview/automatic component generation.png
new file mode 100644
index 000000000..655dc2678
Binary files /dev/null and b/.preview/automatic component generation.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/built in components.png b/.preview/built in components.png
new file mode 100644
index 000000000..eecf323ed
Binary files /dev/null and b/.preview/built in components.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: ericcraft mh.png b/.preview/contributions: ericcraft mh.png
new file mode 100644
index 000000000..3380cd3b2
Binary files /dev/null and b/.preview/contributions: ericcraft mh.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/creating and using components.png b/.preview/creating and using components.png
new file mode 100644
index 000000000..df29ba06b
Binary files /dev/null and b/.preview/creating and using components.png differ
diff --git a/.preview/deployment and optimization.png b/.preview/deployment and optimization.png
new file mode 100644
index 000000000..6667bdd36
Binary files /dev/null and b/.preview/deployment and optimization.png differ
diff --git a/.preview/ericcraft mh: quicklook vertical image tracker.png b/.preview/ericcraft mh: quicklook vertical image tracker.png
new file mode 100644
index 000000000..daf3619e5
Binary files /dev/null and b/.preview/ericcraft mh: quicklook vertical image tracker.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..4f9c427ce
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: custom vr button that appears only on headsets and not on mobile phones.png b/.preview/llllkatjallll: custom vr button that appears only on headsets and not on mobile phones.png
new file mode 100644
index 000000000..a63d90a5d
Binary files /dev/null and b/.preview/llllkatjallll: custom vr button that appears only on headsets and not on mobile phones.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/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 engine for blender.png b/.preview/needle engine for blender.png
new file mode 100644
index 000000000..8ae37d31d
Binary files /dev/null and b/.preview/needle engine for blender.png differ
diff --git a/.preview/needle engine.png b/.preview/needle engine.png
new file mode 100644
index 000000000..c97f572a5
Binary files /dev/null and b/.preview/needle engine.png differ
diff --git a/.preview/needle.png b/.preview/needle.png
new file mode 100644
index 000000000..69b6bb15c
Binary files /dev/null and b/.preview/needle.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/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/robyer1: microphone access in a browser window and streamed playback.png b/.preview/robyer1: microphone access in a browser window and streamed playback.png
new file mode 100644
index 000000000..3fe0a2733
Binary files /dev/null and b/.preview/robyer1: microphone access in a browser window and streamed playback.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..2d4233f08
Binary files /dev/null and b/.preview/scripting examples.png differ
diff --git a/.preview/scripting in needle engine.png b/.preview/scripting in needle engine.png
new file mode 100644
index 000000000..a3a1a8535
Binary files /dev/null and b/.preview/scripting in needle engine.png differ
diff --git a/.preview/scripting introduction for unity developers.png b/.preview/scripting introduction for unity developers.png
new file mode 100644
index 000000000..69ca9d938
Binary files /dev/null and b/.preview/scripting introduction for unity developers.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/using needle engine directly from html.png b/.preview/using needle engine directly from html.png
new file mode 100644
index 000000000..a3c202165
Binary files /dev/null and b/.preview/using needle engine directly from html.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..b9dd744a4
--- /dev/null
+++ b/404.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+ Documentation
+
+
+
+
+
`,1);function p(i,c){return e(),t("div",null,[s(" scripting "),o,s(" technical overview "),l])}const g=n(r,[["render",p],["__file","backlog-mermaid.html.vue"]]);export{g as default};
diff --git a/assets/backlog.html-0991873b.js b/assets/backlog.html-0991873b.js
new file mode 100644
index 000000000..072a8503c
--- /dev/null
+++ b/assets/backlog.html-0991873b.js
@@ -0,0 +1 @@
+import{_ as i,M as a,p as r,q as c,R as e,t,N as o,a1 as s}from"./framework-f6820c83.js";const l={},d=s('
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/backlog.html-5f538ac1.js b/assets/backlog.html-5f538ac1.js
new file mode 100644
index 000000000..6d9ed0dfd
--- /dev/null
+++ b/assets/backlog.html-5f538ac1.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-66ba7b75","path":"/backlog.html","title":"Documentation Backlog","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/backlog.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":"Recommended Unity configuration","slug":"recommended-unity-configuration","link":"#recommended-unity-configuration","children":[]},{"level":2,"title":"Supported Unity configurations","slug":"supported-unity-configurations","link":"#supported-unity-configurations","children":[]},{"level":2,"title":"Source Control","slug":"source-control","link":"#source-control","children":[]},{"level":2,"title":"Authentication","slug":"authentication","link":"#authentication","children":[]}],"git":{"updatedTime":1669763662000},"filePathRelative":"_backlog.md"}');export{e as data};
diff --git a/assets/buttons.esm-5a367c11.js b/assets/buttons.esm-5a367c11.js
new file mode 100644
index 000000000..52baca35a
--- /dev/null
+++ b/assets/buttons.esm-5a367c11.js
@@ -0,0 +1,5 @@
+/*!
+ * github-buttons v2.29.0
+ * (c) 2024 ใชใคใ
+ * @license BSD-2-Clause
+ */var C=window.document,p=window.Math,k=window.HTMLElement,b=window.XMLHttpRequest,F=function(e,t){for(var r=0,o=e.length;r'}}},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:''}}}},oe=function(e,t){e=Z(e).replace(/^octicon-/,""),m(x,e)||(e="mark-github");var r=t>=24&&24 in x[e].heights?24:16,o=x[e].heights[r];return'"},y={},re=function(e,t){var r=y[e]||(y[e]=[]);if(!(r.push(t)>1)){var o=V(function(){for(delete y[e];t=r.shift();)t.apply(null,arguments)});if(S){var a=new b;f(a,"abort",o),f(a,"error",o),f(a,"load",function(){var s;try{s=JSON.parse(this.responseText)}catch(d){o(d);return}o(this.status!==200,s)}),a.open("GET",e),a.send()}else{var n=this||window;n._=function(s){n._=null,o(s.meta.status!==200,s.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,t,r){var o=A(e.ownerDocument),a=e.appendChild(o("style",{type:"text/css"})),n=J+te(t["data-color-scheme"]);a.styleSheet?a.styleSheet.cssText=n:a.appendChild(e.ownerDocument.createTextNode(n));var c=Z(t["data-size"])==="large",l=o("a",{className:"btn",href:t.href,rel:"noopener",target:"_blank",title:t.title||void 0,"aria-label":t["aria-label"]||void 0,innerHTML:oe(t["data-icon"],c?16:14)+" "},[o("span",{},[t["data-text"]||""])]),s=e.appendChild(o("div",{className:"widget"+(c?" widget-lg":"")},[l])),d=l.hostname.replace(/\.$/,"");if(("."+d).substring(d.length-u.length)!=="."+u){l.removeAttribute("href"),r(s);return}var i=(" /"+l.pathname).split(/\/+/);if(((d===u||d==="gist."+u)&&i[3]==="archive"||d===u&&i[3]==="releases"&&(i[4]==="download"||i[4]==="latest"&&i[5]==="download")||d==="codeload."+u)&&(l.target="_top"),Z(t["data-show-count"])!=="true"||d!==u||i[1]==="marketplace"||i[1]==="sponsors"||i[1]==="orgs"||i[1]==="users"||i[1]==="-"){r(s);return}var v,h;if(!i[2]&&i[1])h="followers",v="?tab=followers";else if(!i[3]&&i[2])h="stargazers_count",v="/stargazers";else if(!i[4]&&i[3]==="subscription")h="subscribers_count",v="/watchers";else if(!i[4]&&i[3]==="fork")h="forks_count",v="/forks";else if(i[3]==="issues")h="open_issues_count",v="/issues";else{r(s);return}var D=i[2]?"/repos/"+i[1]+"/"+i[2]:"/users/"+i[1];re.call(this,P+D,function(B,L){if(!B){var w=L[h];s.appendChild(o("a",{className:"social-count",href:L.html_url+v,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,",")]))}r(s)})},M=window.devicePixelRatio||1,z=function(e){return(M>1?p.ceil(p.round(e*M)/M*2)/2:p.ceil(e))||0},ae=function(e){var t=e.offsetWidth,r=e.offsetHeight;if(e.getBoundingClientRect){var o=e.getBoundingClientRect();t=p.max(t,z(o.width)),r=p.max(r,z(o.height))}return[t,r]},H=function(e,t){e.style.width=t[0]+"px",e.style.height=t[1]+"px"},ne=function(e,t){if(!(e==null||t==null))if(e.getAttribute&&(e=q(e)),R){var r=_("span");T(r.attachShadow({mode:"closed"}),e,function(){t(r)})}else{var o=_("iframe",{src:"javascript:0",title:e.title||void 0,allowtransparency:!0,scrolling:"no",frameBorder:0});H(o,[0,0]),o.style.border="none";var a=function(){var n=o.contentWindow,c;try{c=n.document.body}catch{C.body.appendChild(o.parentNode.removeChild(o));return}E(o,"load",a),T.call(n,c,e,function(l){var s=ae(l);o.parentNode.removeChild(o),I(o,"load",function(){H(o,s)}),o.src=N+"#"+(o.name=W(e)),t(o)})};f(o,"load",a),C.body.appendChild(o)}};export{ne as render};
diff --git a/assets/component-compiler.html-7fadcdfe.js b/assets/component-compiler.html-7fadcdfe.js
new file mode 100644
index 000000000..f15a35913
--- /dev/null
+++ b/assets/component-compiler.html-7fadcdfe.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-f6820c83.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-compiler.html-a9955c85.js b/assets/component-compiler.html-a9955c85.js
new file mode 100644
index 000000000..09881267f
--- /dev/null
+++ b/assets/component-compiler.html-a9955c85.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-5572989e","path":"/component-compiler.html","title":"Automatic Component Generation","lang":"en-US","frontmatter":{"title":"Automatic Component Generation","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/automatic component generation.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":{"updatedTime":1725399379000},"filePathRelative":"component-compiler.md"}');export{e as data};
diff --git a/assets/component-reference.html-47d337de.js b/assets/component-reference.html-47d337de.js
new file mode 100644
index 000000000..970ca5eb2
--- /dev/null
+++ b/assets/component-reference.html-47d337de.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-d8eff192","path":"/component-reference.html","title":"Built-in Components","lang":"en-US","frontmatter":{"title":"Built-in Components","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/built in components.png"}],["meta",{"name":"og:description","content":"---\\nHere is a overview of some of the components that we provide. Many of them map to components and functionality in Unity, Blender or other integrations.\\nFor a complete list please have a look at our API docs.\\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. Many of them map to components and functionality in Unity, Blender or other integrations.\\nFor a complete list please have a look at our API docs.\\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":{"updatedTime":1725399379000},"filePathRelative":"component-reference.md"}`);export{e as data};
diff --git a/assets/component-reference.html-7433ba9c.js b/assets/component-reference.html-7433ba9c.js
new file mode 100644
index 000000000..9a0e2a723
--- /dev/null
+++ b/assets/component-reference.html-7433ba9c.js
@@ -0,0 +1 @@
+import{_ as l,M as i,p as s,q as c,R as t,t as e,N as o,V as a,a1 as n}from"./framework-f6820c83.js";const h={},u=t("p",null,"Here is a overview of some of the components that we provide. Many of them map to components and functionality in Unity, Blender or other integrations.",-1),p={href:"https://engine.needle.tools/docs/api/latest",target:"_blank",rel:"noopener noreferrer"},m=t("p",null,"You can always add your own components or add wrappers for Unity components we haven't provided yet.",-1),b=n('
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
',7),g={href:"https://www.npmjs.com/package/postprocessing",target:"_blank",rel:"noopener noreferrer"},f=t("ul",null,[t("li",null,[t("strong",null,"Unity only"),e(": "),t("em",null,"Note that Postprocessing effects using a Volume in Unity is only supported with URP")])],-1),y=t("thead",null,[t("tr",null,[t("th",null,"Effect Name"),t("th")])],-1),_=t("tr",null,[t("td",null,"Antialiasing"),t("td",null,[t("em",null,"extra Unity Component")])],-1),w=t("tr",null,[t("td",null,"Bloom"),t("td",null,[t("em",null,"via Volume asset")])],-1),x=t("tr",null,[t("td",null,"Chromatic Aberration"),t("td",null,[t("em",null,"via Volume asset")])],-1),R=t("tr",null,[t("td",null,"Color Adjustments / Color Correction"),t("td",null,[t("em",null,"via Volume asset")])],-1),v=t("tr",null,[t("td",null,"Depth Of Field"),t("td",null,[t("em",null,"via Volume asset")])],-1),k=t("tr",null,[t("td",null,"Vignette"),t("td",null,[t("em",null,"via Volume asset")])],-1),S=t("tr",null,[t("td",null,"ToneMappingEffect"),t("td",null,[t("em",null,"via Volume asset or separate component")])],-1),A=t("tr",null,[t("td",null,"Pixelation"),t("td")],-1),D=t("tr",null,[t("td",null,"Screenspace Ambient Occlusion N8"),t("td")],-1),C=t("tr",null,[t("td",null,"Screenspace Ambient Occlusion"),t("td")],-1),U=t("tr",null,[t("td",null,"Tilt Shift Effect"),t("td")],-1),j=t("tr",null,[t("td",null,"SharpeningEffect"),t("td")],-1),N=t("td",null,[t("em",null,"Your custom effect")],-1),I={href:"https://stackblitz.com/edit/needle-engine-custom-postprocessing-effect",target:"_blank",rel:"noopener noreferrer"},P=n('
',2),V=t("thead",null,[t("tr",null,[t("th",null,"Name"),t("th",null,"Description")])],-1),L=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),B=t("code",null,"USDZExporter",-1),M=t("td",null,"Add to enable USD and Quicklook support",-1),X=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),O=t("tr",null,[t("td",null,[t("code",null,"WebARSessionRoot")]),t("td",null,"Handles placement and scale of your scene in AR mode")],-1),W=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),F=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),H=t("tr",null,[t("td",null,[t("code",null,"WebXRPlaneTracking")]),t("td",null,"Create plane meshes or colliders for tracked planes")],-1),G=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),q=t("tr",null,[t("td",null,[t("code",null,"XRControllerMovement")]),t("td",null,"Can be added to provide default movement and teleport controls")],-1),z=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),Y=n('
Unity's UI system. Needs to be in World Space mode right now.
Text (Legacy)
Render Text using Unity's UI Text component. 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. Note: In Unity make sure to use the Legacy/Text component (TextMeshPro is not supported at the moment)
Button
Receives click events - use the onClick event to react to it. It can be added too 3D scene objects as well. Note: Make sure to use the Legacy/Text component in the Button (or create the Button via the UI/Legacy/Button Unity context menu since TextMeshPro is not supported at the moment)
Image
Renders a sprite image
RawImage
Renders a texture
InputField
Allows text input
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 J(K,$){const d=i("ExternalLinkIcon"),r=i("RouterLink");return s(),c("div",null,[u,t("p",null,[e("For a complete list please have a look at our "),t("a",p,[e("API docs"),o(d)]),e(".")]),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(d)]),e(" under the hood. This means you can also easily add your own custom effects and get an automatically optimized postprocessing pass.")]),f,t("table",null,[y,t("tbody",null,[_,w,x,R,v,k,S,A,D,C,U,j,t("tr",null,[N,t("td",null,[t("a",I,[e("See example on stackblitz"),o(d)])])])])]),P,t("p",null,[e("Physics is implemented using "),t("a",T,[e("Rapier"),o(d)]),e(".")]),E,t("p",null,[o(r,{to:"/xr.html"},{default:a(()=>[e("Read the XR docs")]),_:1})]),t("table",null,[V,t("tbody",null,[L,t("tr",null,[t("td",null,[o(r,{to:"/everywhere-actions.html"},{default:a(()=>[B]),_:1})]),M]),X,O,W,F,H,G,q,z])]),Y,t("p",null,[e("Spatial UI components are mapped from Unity UI (Canvas, not UI Toolkit) to "),t("a",Z,[e("three-mesh-ui"),o(d)]),e(". UI can be animated.")]),Q])}const et=l(h,[["render",J],["__file","component-reference.html.vue"]]);export{et as default};
diff --git a/assets/contribution-header-f6071865.js b/assets/contribution-header-f6071865.js
new file mode 100644
index 000000000..ee31146e0
--- /dev/null
+++ b/assets/contribution-header-f6071865.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-f6820c83.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-41a12262.js b/assets/contribution-listentry-41a12262.js
new file mode 100644
index 000000000..683772d2a
--- /dev/null
+++ b/assets/contribution-listentry-41a12262.js
@@ -0,0 +1 @@
+import{_ as e,p as n,q as r,R as s,v as c}from"./framework-f6820c83.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-754df1c1.js b/assets/contribution-preview-754df1c1.js
new file mode 100644
index 000000000..c9fd63f66
--- /dev/null
+++ b/assets/contribution-preview-754df1c1.js
@@ -0,0 +1 @@
+import{_ as s,p as c,q as r,R as t,v as a,s as _}from"./framework-f6820c83.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-f0bcc61a.js b/assets/contributions-author-f0bcc61a.js
new file mode 100644
index 000000000..e56f243a3
--- /dev/null
+++ b/assets/contributions-author-f0bcc61a.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-f6820c83.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-5f531929.js b/assets/contributions-overview-5f531929.js
new file mode 100644
index 000000000..77f76fbfa
--- /dev/null
+++ b/assets/contributions-overview-5f531929.js
@@ -0,0 +1 @@
+import{_ as t,p as s,q as o,s as n}from"./framework-f6820c83.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-6b449b0c.js b/assets/copyright-6b449b0c.js
new file mode 100644
index 000000000..bd8888609
--- /dev/null
+++ b/assets/copyright-6b449b0c.js
@@ -0,0 +1 @@
+import{_ as a,p as c,q as s,t as _,a9 as p,aa as r,R as t}from"./framework-f6820c83.js";const n={},o=e=>(p("data-v-682f5e2f"),e=e(),r(),e),d={class:"footer"},i=o(()=>t("a",{target:"_blank",href:"https://needle.tools/contact"},"About ยท ",-1)),l=o(()=>t("a",{target:"_blank",href:"https://needle.tools/contact#privacy-policy"},"Privacy Policy",-1));function f(e,h,u,y,v,g){return c(),s("div",d,[_(" ยฉ2024 Needle Tools GmbH ยท "),i,l])}const x=a(n,[["render",f],["__scopeId","data-v-682f5e2f"],["__file","copyright.vue"]]);export{x 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-0370ea20.js b/assets/debugging.html-0370ea20.js
new file mode 100644
index 000000000..69daf86f3
--- /dev/null
+++ b/assets/debugging.html-0370ea20.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":{"updatedTime":1700724672000},"filePathRelative":"debugging.md"}');export{e as data};
diff --git a/assets/debugging.html-b90d9481.js b/assets/debugging.html-b90d9481.js
new file mode 100644
index 000000000..b570a4e0a
--- /dev/null
+++ b/assets/debugging.html-b90d9481.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-f6820c83.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-679a59a7.js b/assets/deployment.html-679a59a7.js
new file mode 100644
index 000000000..5df6959b7
--- /dev/null
+++ b/assets/deployment.html-679a59a7.js
@@ -0,0 +1,5 @@
+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-f6820c83.js";const a="/docs/imgs/unity-texture-compression.jpg",g="/docs/imgs/unity-texture-compression-options.jpg",y="/docs/imgs/unity-mesh-compression-component.jpg",b="/docs/imgs/unity-mesh-simplification.jpg",f="/docs/imgs/unity-progressive-textures.jpg",_="/docs/imgs/unity-lods-settings-1.jpg",w="/docs/imgs/unity-lods-settings-2.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",D="/docs/deployment/deploytoftp.jpg",P="/docs/deployment/deploytoftp2.jpg",F="/docs/deployment/deploytoftp3.jpg",I="/docs/deployment/buildoptions_gzip.jpg",G="/docs/deployment/deploytogithubpages.jpg",U="/docs/deployment/deploytofacebookinstantgames.jpg",N="/docs/deployment/deploytofacebookinstantgames-hosting.jpg",B="/docs/deployment/deploytofacebookinstantgames-upload.jpg",C="/docs/deployment/facebookinstantgames-1.jpg",S="/docs/deployment/facebookinstantgames-2.jpg",A="/docs/deployment/facebookinstantgames-3.jpg",E="/docs/imgs/unity-build-window-menu.jpg",O="/docs/imgs/unity-build-window.jpg",z={},L=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.
',4),H={class:"custom-container tip"},M=e("p",{class:"custom-container-title"},"Feel something is missing?",-1),W={href:"https://discord.needle.tools",target:"_blank",rel:"noopener noreferrer"},q=e("h2",{id:"development-builds",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#development-builds","aria-hidden":"true"},"#"),t(" Development Builds")],-1),Y=e("p",null,"See guides above on how to access the options from within your Editor (e.g. Unity or Blender).",-1),K={href:"https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html",target:"_blank",rel:"noopener noreferrer"},R={href:"https://google.github.io/draco/",target:"_blank",rel:"noopener noreferrer"},V=e("p",null,"We generally recommend making production builds for optimized file size and loading speed (see more information below).",-1),X=e("h2",{id:"production-builds",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#production-builds","aria-hidden":"true"},"#"),t(" Production Builds")],-1),J={href:"https://github.com/KhronosGroup/KTX-Software/releases",target:"_blank",rel:"noopener noreferrer"},Q={href:"https://github.com/KhronosGroup/KTX-Software/releases",target:"_blank",rel:"noopener noreferrer"},Z=e("br",null,null,-1),$=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),ee={class:"custom-container details"},te=e("summary",null,"Advanced: Custom glTF extensions",-1),oe=e("code",null,"gltf-transform",-1),ie={href:"https://www.npmjs.com/package/@needle-tools/gltf-build-pipeline",target:"_blank",rel:"noopener noreferrer"},ne=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
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 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:
Since Needle Engine 3.36 we automatically generate LOD meshes and switch between them at runtime. LODs are loaded on demand and only when needed so so this feature both reduces your loading time as well as performance.
Key Beneftis
Faster initial loading time
Faster rendering time due to less vertices on screen on average
Faster raycasting due to the use of LOD meshes
You can either disable LOD generation for your whole project in the Progressive Loading Settings component or in the Mesh Importer settings.
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),re=e("code",null,"Create new Glitch Remix",-1),le=e("code",null,"there was an error starting the editor",-1),de=e("strong",null,"OK",-1),ce={href:"https://glitch.com/",target:"_blank",rel:"noopener noreferrer"},pe=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 by deploying to existing projects.
',4),he={href:"https://github.com/needle-engine/nextjs-sample",target:"_blank",rel:"noopener noreferrer"},ue=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),me={class:"custom-container details"},ge=e("summary",null,"How do I deploy to itch.io from Unity?",-1),ye={href:"https://itch.io/game/new",target:"_blank",rel:"noopener noreferrer"},be=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),fe=e("h4",{id:"optional-settings",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#optional-settings","aria-hidden":"true"},"#"),t(" Optional settings")],-1),_e=e("p",null,[e("img",{src:"https://user-images.githubusercontent.com/5083203/191217263-355d9b72-5431-4170-8eca-bfbbb39ae810.png",alt:"image"})],-1),we={class:"custom-container details"},ve=e("summary",null,"Itch.io: failed to find index.html",-1),xe={id:"failed-to-find-index.html",tabindex:"-1"},ke=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),Te=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.
To enable gzip compression on your FTP server you can create a file named .htaccess in the directory you want to upload to (or a parent directory). Insert the following code into your .htaccess file and save/upload it to your server:
`,7),je={class:"custom-container details"},De=e("summary",null,"How do I deploy to Github Pages from Unity?",-1),Pe=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:G,alt:"Deploy To github pages component"})],-1),Fe=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),Ie={class:"custom-container details"},Ge=e("summary",null,"How do I create a app on Facebook (with Instant Games capabilities)",-1),Ue={href:"https://developers.facebook.com/apps/creation/",target:"_blank",rel:"noopener noreferrer"},Ne=e("code",null,"Other",-1),Be=e("code",null,"Next",-1),Ce=e("img",{src:C,alt:"Create facebook instant games app"},null,-1),Se=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),Ae=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),Ee={href:"https://developers.facebook.com/docs/games/build/instant-games",target:"_blank",rel:"noopener noreferrer"},Oe=e("br",null,null,-1),ze=e("strong",null,"Note",-1),Le=e("br",null,null,-1),He=n('
In Unity open File/Build Settings and select Needle Engine for options:
To build your web project for uploading to any 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.
To locally preview your final build you can use the Preview Build button at the bottom of the window. This button will first perform a regular build and then start a local server in the directory with the final files so you can see what you get once you upload these files to your webserver.
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).
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
',15);function Me(We,qe){const i=s("ExternalLinkIcon"),r=s("RouterLink"),l=s("video-embed");return h(),u("div",null,[L,e("div",H,[M,e("p",null,[t("Please let us know in our "),e("a",W,[t("discord"),o(i)]),t("!")])]),q,Y,e("p",null,[t("The main difference to a production build is that it does not perform "),e("a",K,[t("ktx2"),o(i)]),t(" and "),e("a",R,[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.")]),V,X,e("p",null,[t("To make a production build, you need to have "),e("a",J,[t("toktx"),o(i)]),t(" installed, which provides texture compression using the KTX2 supercompression format. Please go to the "),e("a",Q,[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."),Z,$]),e("details",ee,[te,e("p",null,[t("If you plan on adding your own custom glTF extensions, building for production requires handling those in "),oe,t(". See "),e("a",ie,[t("@needle-tools/gltf-build-pipeline"),o(i)]),t(" for reference.")])]),ne,e("p",null,[e("a",se,[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.")]),ae,e("p",null,[t("If you click "),re,t(" and the browser shows an error like "),le,t(" you can click "),de,t(". Then go to "),e("a",ce,[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.")]),pe,e("p",null,[t("See our "),e("a",he,[t("sample project"),o(i)]),t(" for the project configuration")]),ue,e("details",me,[ge,e("ol",null,[e("li",null,[e("p",null,[t("Create a new project on "),e("a",ye,[t("itch.io"),o(i)])])]),be]),fe,_e]),e("details",we,[ve,e("h4",xe,[o(r,{class:"header-anchor",to:"/#failed-to-find-index.html","aria-hidden":"true"},{default:m(()=>[t("#")]),_:1}),t(" Failed to find index.html")]),ke]),Te,e("details",je,[De,Pe,o(l,{src:"https://www.youtube.com/watch?v=Vyk3cWB6u-c"})]),Fe,e("details",Ie,[Ge,e("ol",null,[e("li",null,[e("p",null,[e("a",Ue,[t("Create a new app"),o(i)]),t(" and select "),Ne,t(". Then click "),Be,Ce])]),Se,Ae]),e("p",null,[t("Here you can find "),e("a",Ee,[t("the official instant games documentation"),o(i)]),t(" on facebook."),Oe,ze,t(" that all you have to do is to create an app with instant games capabilities."),Le,t(" We will take care of everything else and no manual adjustments to your Needle Engine website are required.")])]),He])}const Ve=p(z,[["render",Me],["__file","deployment.html.vue"]]);export{Ve as default};
diff --git a/assets/deployment.html-a77d8c0c.js b/assets/deployment.html-a77d8c0c.js
new file mode 100644
index 000000000..f1f63902c
--- /dev/null
+++ b/assets/deployment.html-a77d8c0c.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-4d21151b","path":"/deployment.html","title":"Deployment and Optimization","lang":"en-US","frontmatter":{"title":"Deployment and Optimization","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/deployment and optimization.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":"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":3,"title":"Texture compression","slug":"texture-compression","link":"#texture-compression","children":[]},{"level":3,"title":"Mesh compression","slug":"mesh-compression","link":"#mesh-compression","children":[]},{"level":3,"title":"Progressive Textures","slug":"progressive-textures","link":"#progressive-textures","children":[]},{"level":3,"title":"Automatic Mesh LODs (Level of Detail)","slug":"automatic-mesh-lods-level-of-detail","link":"#automatic-mesh-lods-level-of-detail","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":"Build To Folder","slug":"build-to-folder","link":"#build-to-folder","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":{"updatedTime":1725399379000},"filePathRelative":"deployment.md"}');export{e as data};
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-440cc1d9.js b/assets/everywhere-actions.html-440cc1d9.js
new file mode 100644
index 000000000..6801714c7
--- /dev/null
+++ b/assets/everywhere-actions.html-440cc1d9.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":[]}]},{"level":2,"title":"Further reading","slug":"further-reading","link":"#further-reading","children":[]}],"git":{"updatedTime":1718696909000},"filePathRelative":"everywhere-actions.md"}`);export{e as data};
diff --git a/assets/everywhere-actions.html-7590cd35.js b/assets/everywhere-actions.html-7590cd35.js
new file mode 100644
index 000000000..d022df9fb
--- /dev/null
+++ b/assets/everywhere-actions.html-7590cd35.js
@@ -0,0 +1,23 @@
+import{_ as c,M as o,p as d,q as l,N as n,R as e,t,V as p,a1 as i}from"./framework-f6820c83.js";const u="/docs/imgs/everywhere-actions-component-menu.gif",h={},m=i('
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),S=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),B={href:"https://developer.apple.com/documentation/arkit/usdz_schemas_for_ar/actions_and_triggers",target:"_blank",rel:"noopener noreferrer"},C=i(`
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.
The following pages provide more examples and samples that you can test and explore right now:
`,11),T={href:"https://engine.needle.tools/projects/ar-showcase/",target:"_blank",rel:"noopener noreferrer"},R={href:"https://engine.needle.tools/samples/?overlay=samples&tag=everywhere+actions",target:"_blank",rel:"noopener noreferrer"};function j(O,E){const a=o("sample"),r=o("RouterLink"),s=o("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(r,{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,S,e("p",null,[t("Triggers and Actions for QuickLook are based on "),e("a",B,[t("Apple's Preliminary Interactive USD Schemas"),n(s)])]),C,e("ul",null,[e("li",null,[t("Visit our "),e("a",T,[t("AR Showcase Website"),n(s)]),t(" that has many interactive AR examples with a focus on iOS AR & Quicklook")]),e("li",null,[e("a",R,[t("Needle Engine Everywhere Action Samples"),n(s)])])])])}const L=c(h,[["render",j],["__file","everywhere-actions.html.vue"]]);export{L as default};
diff --git a/assets/examples.html-1d035fb3.js b/assets/examples.html-1d035fb3.js
new file mode 100644
index 000000000..f97e37bf8
--- /dev/null
+++ b/assets/examples.html-1d035fb3.js
@@ -0,0 +1 @@
+import{_ as a,M as r,p as s,q as d,R as e,t,N as o,a1 as l}from"./framework-f6820c83.js";const i={},c=l('
',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 โก"),o(n)]),t(" โ by Needle")]),p,u,e("p",null,[e("a",f,[t("Bike Configurator โก"),o(n)]),t(" โ by Needle")]),_,b,e("p",null,[e("a",m,[t("Sandbox Template โก"),o(n)]),t(" โ by Needle")]),g,x,e("p",null,[e("a",k,[t("Songs of Cultures โก"),o(n)]),t(" โ by A.MUSE")]),y,w,e("p",null,[e("a",N,[t("Pokรฉmon Card โก"),o(n)]),t(" โ Scene from Alex Ameye โข "),e("a",S,[t("Original Blog Post by Alex โก"),o(n)])]),v,E,e("p",null,[e("a",P,[t("Encryption in Space โก"),o(n)]),t(" โ by Katja Rempel & Nick Jwu")]),B,C,e("p",null,[e("a",j,[t("Physics Playground โก"),o(n)]),t(" โ Scene from Bruno Simon")]),V])}const M=a(i,[["render",A],["__file","examples.html.vue"]]);export{M as default};
diff --git a/assets/examples.html-b9380b47.js b/assets/examples.html-b9380b47.js
new file mode 100644
index 000000000..fac33e819
--- /dev/null
+++ b/assets/examples.html-b9380b47.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":{"updatedTime":1724755401000},"filePathRelative":"examples.md"}');export{e as data};
diff --git a/assets/export.html-4f710d49.js b/assets/export.html-4f710d49.js
new file mode 100644
index 000000000..53c209bb6
--- /dev/null
+++ b/assets/export.html-4f710d49.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":{"updatedTime":1724755401000},"filePathRelative":"export.md"}`);export{e as data};
diff --git a/assets/export.html-a783fce9.js b/assets/export.html-a783fce9.js
new file mode 100644
index 000000000..5c82b821d
--- /dev/null
+++ b/assets/export.html-a783fce9.js
@@ -0,0 +1,26 @@
+import{_ as r,M as o,p as l,q as c,R as t,t as e,N as a,V as d,a1 as s}from"./framework-f6820c83.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?utm_source=needle_docs&utm_content=export_scenes",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=o("ExternalLinkIcon"),i=o("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(i,{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-e7435103.js b/assets/faq.html-e7435103.js
new file mode 100644
index 000000000..83f0db755
--- /dev/null
+++ b/assets/faq.html-e7435103.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.jpeg"}],["meta",{"name":"og:description","content":"---"}]],"description":"---"},"headers":[{"level":2,"title":"I purchased a license - how can I activate my Needle Engine License?","slug":"i-purchased-a-license-how-can-i-activate-my-needle-engine-license","link":"#i-purchased-a-license-how-can-i-activate-my-needle-engine-license","children":[{"level":3,"title":"Activating the license in Unity","slug":"activating-the-license-in-unity","link":"#activating-the-license-in-unity","children":[]},{"level":3,"title":"Activating the license in Blender","slug":"activating-the-license-in-blender","link":"#activating-the-license-in-blender","children":[]}]},{"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 UI is not rendering Text","slug":"my-ui-is-not-rendering-text","link":"#my-ui-is-not-rendering-text","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":{"updatedTime":1724755401000},"filePathRelative":"faq.md"}`);export{e as data};
diff --git a/assets/faq.html-f846be88.js b/assets/faq.html-f846be88.js
new file mode 100644
index 000000000..cf09e04f6
--- /dev/null
+++ b/assets/faq.html-f846be88.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 h,R as e,t as n,N as t,V as i,a1 as o}from"./framework-f6820c83.js";const p="/docs/imgs/unity-needle-engine-license.jpg",u="/docs/faq/lightmap_encoding.jpg",m={},g=o('
# I purchased a license - how can I activate my Needle Engine License?
Open Edit/Project Settings/Needle to get the Needle Engine plugin settings. At the top of the window you'll find fields for entering your license information.
Email - Enter the email you purchased the license with
Invoice ID - Enter one of the invoice ids that you received by email
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),b=e("strong",null,"try to split up your content into multiple glb files",-1),y=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),j=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"},S=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),T=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),q={href:"http://localhost:8080/docs/getting-started.html#install-these-tools-for-production-builds",target:"_blank",rel:"noopener noreferrer"},M=e("li",null,[e("p",null,[n("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),N=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),C={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"},"#"),n(" Known devices to cause issues:")],-1),O=e("ul",null,[e("li",null,"Lenovo Thinkpad - T495")],-1),L=e("h2",{id:"still-have-questions",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#still-have-questions","aria-hidden":"true"},"#"),n(" Still have questions? ๐ฑ")],-1),U={href:"https://discord.needle.tools",target:"_blank",rel:"noopener noreferrer"},G=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 F(Y,D){const s=r("RouterLink"),a=r("ExternalLinkIcon");return d(),h("div",null,[g,e("p",null,[n("See "),t(s,{to:"/testing.html"},{default:i(()=>[n("the Testing docs")]),_:1}),n(" for information on how to set up a self-signed certificate for a smoother development experience.")]),f,e("p",null,[n("If loading time itself is an issue you can "),b,n(" 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 "),t(s,{to:"/scripting.html#assetreference-and-addressables"},{default:i(()=>[n("Scripting/Addressables in the documentation")]),_:1}),n(".")]),y,e("p",null,[n("Ensure you're following "),e("a",k,[n("best practices for lightmaps"),t(a)]),n(" and read about "),e("a",w,[n("mixing baked and non-baked objects"),t(a)])]),v,e("p",null,[n("Also see the docs on "),e("a",x,[n("mixing baked and non-baked objects"),t(a)]),n(".")]),_,e("ul",null,[j,e("li",null,[n("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,[n("networking backend package"),t(a)]),n(" itself somewhere else where it can scale e.g. Google Cloud.")])]),S,e("ul",null,[e("li",null,[n("Make sure you follow the "),t(s,{to:"/getting-started/#prerequisites"},{default:i(()=>[n("Prerequisites")]),_:1}),n(".")]),I]),T,e("ul",null,[e("li",null,[e("p",null,[n("Make sure to "),e("a",q,[n("download and install toktx"),t(a)])])]),M]),N,e("p",null,[n("If this doesn't fix the problem please ask "),e("a",R,[n("in our discord"),t(a)]),n(".")]),A,e("p",null,[n("Use a detector "),e("a",C,[n("like this one"),t(a)]),n(" 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,O,L,e("p",null,[e("a",U,[n("Ask in our friendly discord community"),t(a)])]),G])}const B=c(m,[["render",F],["__file","faq.html.vue"]]);export{B as default};
diff --git a/assets/features-overview.html-04daa736.js b/assets/features-overview.html-04daa736.js
new file mode 100644
index 000000000..c79bfa7e4
--- /dev/null
+++ b/assets/features-overview.html-04daa736.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":{"updatedTime":1725399379000},"filePathRelative":"features-overview.md"}');export{e as data};
diff --git a/assets/features-overview.html-103369e6.js b/assets/features-overview.html-103369e6.js
new file mode 100644
index 000000000..c00ba7779
--- /dev/null
+++ b/assets/features-overview.html-103369e6.js
@@ -0,0 +1 @@
+import{_ as d,M as l,p as c,q as h,R as e,t,N as n,V as o,a1 as u}from"./framework-f6820c83.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),_={href:"https://needle.tools",target:"_blank",rel:"noopener noreferrer"},g={href:"https://engine.needle.tools/samples",target:"_blank",rel:"noopener noreferrer"},f={class:"table-of-contents"},b=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),y=e("p",null,"Both PBR Materials and Custom shaders created with Shader Graph or other systems can be exported.",-1),w=e("img",{src:"https://user-images.githubusercontent.com/5083203/186012027-9bbe3944-fa56-41fa-bfbb-c989fa87aebb.png"},null,-1),k={href:"https://unity.com/features/shader-graph",target:"_blank",rel:"noopener noreferrer"},v=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),x=e("strong",null,"Interactive AR on both Android and iOS",-1),S=e("h2",{id:"lightmaps",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#lightmaps","aria-hidden":"true"},"#"),t(" Lightmaps")],-1),A=e("p",null,[e("img",{src:"https://user-images.githubusercontent.com/5083203/186163693-093c7ae2-96eb-4d75-b98f-bf19f78032ff.gif",alt:"lightmaps"})],-1),E=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),N=e("strong",null,"Note",-1),R={href:"https://assetstore.unity.com/packages/tools/level-design/bakery-gpu-lightmapper-122218",target:"_blank",rel:"noopener noreferrer"},U={href:"https://fwd.needle.tools/needle-engine/docs/lightmaps",target:"_blank",rel:"noopener noreferrer"},C=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),B=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),T={href:"https://fwd.needle.tools/needle-engine/docs/networking",target:"_blank",rel:"noopener noreferrer"},I={href:"https://fwd.needle.tools/needle-engine/docs/scripting",target:"_blank",rel:"noopener noreferrer"},P=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),M=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),q=e("h3",{id:"animator",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#animator","aria-hidden":"true"},"#"),t(" Animator")],-1),L=e("img",{src:"https://user-images.githubusercontent.com/5083203/186011302-176524b3-e8e5-4e6e-9b77-7faf3561bb15.png"},null,-1),O={href:"https://docs.unity3d.com/Manual/class-AnimatorController.html",target:"_blank",rel:"noopener noreferrer"},D=e("code",null,"OnStateEnter",-1),V=e("code",null,"OnStateUpdate",-1),F=e("code",null,"OnStateExit",-1),j=e("blockquote",null,[e("p",null,[e("strong",null,"Note"),t(": Sub-states and Blend Trees are not supported.")])],-1),G=e("h3",{id:"timeline",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#timeline","aria-hidden":"true"},"#"),t(" Timeline")],-1),W=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),z={href:"https://unity.com/features/timeline",target:"_blank",rel:"noopener noreferrer"},H=e("br",null,null,-1),J=e("blockquote",null,[e("p",null,[e("strong",null,"Note"),t(": Sub-Timelines are currently not supported.")])],-1),X=e("strong",null,"Note",-1),Y={href:"https://github.com/needle-tools/needle-engine-modules/tree/main/package/TimelineHtml",target:"_blank",rel:"noopener noreferrer"},K=e("h2",{id:"physics",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#physics","aria-hidden":"true"},"#"),t(" Physics")],-1),Q=e("p",null,"Use Rigidbodies, Mesh Colliders, Box Colliders and SphereColliders to add some juicy physics to your world.",-1),Z=e("h2",{id:"ui",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#ui","aria-hidden":"true"},"#"),t(" UI")],-1),$=e("p",null,"Building UI using Unity's UI canvas system is in development. Features currently include exporting Text (including fonts), Images, Buttons.",-1),ee=e("h2",{id:"particles",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#particles","aria-hidden":"true"},"#"),t(" Particles")],-1),te=e("br",null,null,-1),ne={href:"https://engine.needle.tools/samples/particles",target:"_blank",rel:"noopener noreferrer"},oe=e("h2",{id:"postprocessing",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#postprocessing","aria-hidden":"true"},"#"),t(" PostProcessing")],-1),ae=e("h2",{id:"editor-integrations",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#editor-integrations","aria-hidden":"true"},"#"),t(" Editor Integrations")],-1),re=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),ie=e("h2",{id:"scripting",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#scripting","aria-hidden":"true"},"#"),t(" Scripting")],-1),se={href:"https://fwd.needle.tools/needle-engine/docs/npmdef",target:"_blank",rel:"noopener noreferrer"},le={href:"https://fwd.needle.tools/needle-engine/docs/component-compiler",target:"_blank",rel:"noopener noreferrer"},de=e("a",{href:"scripting"},"Scripting Reference",-1),ce={href:"https://fwd.needle.tools/needle-engine/docs/npmdef",target:"_blank",rel:"noopener noreferrer"},he=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),ue=e("li",null,"PostProcessing โ Bloom, Screenspace Ambient Occlusion, Depth of Field, Color Correction...",-1),pe=e("li",null,"EditorSync โ Live synchronize editing in Unity to the running three.js application for local development",-1),me=u('
',3);function _e(ge,fe){const a=l("ExternalLinkIcon"),r=l("router-link"),i=l("RouterLink"),s=l("sample");return c(),h("div",null,[m,e("p",null,[t("Needle Engine is a fully fledged 3D engine that runs in the browser. It comes with all the features you'd expect from a modern 3D engine, and more. If you haven't yet, take a look at our "),e("a",_,[t("Homepage"),n(a)]),t(" and our "),e("a",g,[t("Samples and Showcase"),n(a)]),t(".")]),e("nav",f,[e("ul",null,[e("li",null,[n(r,{to:"#shaders-and-materials"},{default:o(()=>[t("Shaders and Materials")]),_:1})]),e("li",null,[n(r,{to:"#crossplatform-vr-ar-mobile-desktop"},{default:o(()=>[t("Crossplatform: VR, AR, Mobile, Desktop")]),_:1})]),e("li",null,[n(r,{to:"#lightmaps"},{default:o(()=>[t("Lightmaps")]),_:1})]),e("li",null,[n(r,{to:"#multiplayer-and-networking"},{default:o(()=>[t("Multiplayer and Networking")]),_:1})]),e("li",null,[n(r,{to:"#animations-and-sequencing"},{default:o(()=>[t("Animations and Sequencing")]),_:1}),e("ul",null,[e("li",null,[n(r,{to:"#animator"},{default:o(()=>[t("Animator")]),_:1})]),e("li",null,[n(r,{to:"#timeline"},{default:o(()=>[t("Timeline")]),_:1})])])]),e("li",null,[n(r,{to:"#physics"},{default:o(()=>[t("Physics")]),_:1})]),e("li",null,[n(r,{to:"#ui"},{default:o(()=>[t("UI")]),_:1})]),e("li",null,[n(r,{to:"#particles"},{default:o(()=>[t("Particles")]),_:1})]),e("li",null,[n(r,{to:"#postprocessing"},{default:o(()=>[t("PostProcessing")]),_:1})]),e("li",null,[n(r,{to:"#editor-integrations"},{default:o(()=>[t("Editor Integrations")]),_:1})]),e("li",null,[n(r,{to:"#scripting"},{default:o(()=>[t("Scripting")]),_:1})]),e("li",null,[n(r,{to:"#and-there-is-more"},{default:o(()=>[t("And there is more")]),_:1})])])]),b,y,w,e("p",null,[t("Use the node based "),e("a",k,[t("ShaderGraph"),n(a)]),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(i,{to:"/export.html#physically-based-materials-pbr"},{default:o(()=>[t("PBR Materials")]),_:1}),t(" โข "),n(i,{to:"/export.html#custom-shaders"},{default:o(()=>[t("Custom Shaders")]),_:1})]),v,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(i,{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(i,{to:"/everywhere-actions.html"},{default:o(()=>[t("Everywhere Actions")]),_:1}),t(" for "),x,t(".")]),S,A,E,e("blockquote",null,[e("p",null,[N,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",R,[t("Bakery"),n(a)]),t(" thus are also supported.")])]),e("ul",null,[e("li",null,[t("Read more about "),e("a",U,[t("Exporting Lightmaps"),n(a)])])]),C,B,e("ul",null,[e("li",null,[t("Read more about "),e("a",T,[t("Networking"),n(a)]),t(" โข "),e("a",I,[t("Scripting"),n(a)])])]),P,M,e("ul",null,[e("li",null,[t("Read more about "),n(i,{to:"/component-reference.html#animation"},{default:o(()=>[t("Animation Components")]),_:1})])]),q,L,e("p",null,[t("The "),e("a",O,[t("Animator and AnimatorController"),n(a)]),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 "),D,t(", "),V,t(" and "),F,t(" events.")]),j,G,W,e("p",null,[t("We're also translating "),e("a",z,[t("Unity's Timeline"),n(a)]),t(" setup and tracks into a web-ready format."),H,t(" Supported tracks include: AnimationTrack, AudioTrack, ActivationTrack, ControlTrack, SignalTrack.")]),J,e("blockquote",null,[e("p",null,[X,t(": It's possible to "),e("a",Y,[t("export custom timeline tracks"),n(a)]),t(".")])]),e("ul",null,[e("li",null,[t("Read more about "),n(i,{to:"/component-reference.html#animation"},{default:o(()=>[t("Animation Components")]),_:1})])]),K,Q,e("ul",null,[e("li",null,[t("Read more about "),n(i,{to:"/component-reference.html#physics"},{default:o(()=>[t("Physics Components")]),_:1})])]),n(s,{src:"https://engine.needle.tools/samples-uploads/physics-animation/"}),Z,$,e("p",null,[t("See the "),n(i,{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"}),ee,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."),te,t(" See a "),e("a",ne,[t("live sample"),n(a)]),t(" of supported features below:")]),n(s,{src:"https://engine.needle.tools/samples-uploads/particles/"}),oe,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(i,{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/"}),ae,re,ie,e("p",null,[t("Needle Engine uses as "),n(i,{to:"/scripting.html#component-architecture"},{default:o(()=>[t("component based workflow")]),_:1}),t(". Create custom scripts in typescript or javascript. Use our "),e("a",se,[t("modular npm-based package workflow"),n(a)]),t(" integrated into Unity. A "),e("a",le,[t("typescript to C# component compiler"),n(a)]),t(" produces Unity components magically on the fly.")]),e("ul",null,[e("li",null,[t("Read more about "),de,t(" โข "),e("a",ce,[t("Npm Definition Files"),n(a)])])]),he,e("ul",null,[ue,pe,e("li",null,[t("Interactive AR on iOS and Android โ Use our "),n(i,{to:"/everywhere-actions.html"},{default:o(()=>[t("Everywhere Actions")]),_:1}),t(" feature set or build your own")])]),me])}const ye=d(p,[["render",_e],["__file","features-overview.html.vue"]]);export{ye as default};
diff --git a/assets/for-unity-developers.html-36af0499.js b/assets/for-unity-developers.html-36af0499.js
new file mode 100644
index 000000000..1e2f6035e
--- /dev/null
+++ b/assets/for-unity-developers.html-36af0499.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-f6820c83.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();
+ }
+}
+
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"},v=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),b={href:"https://vitejs.dev",target:"_blank",rel:"noopener noreferrer"},k=e("p",null,"Here's some example tech stacks that are possible and that we use Needle Engine with:",-1),f=e("li",null,[e("p",null,[e("strong",null,"Vite + HTML"),n(" โ This is what our default template uses!")])],-1),y=e("strong",null,"Vite + Vue",-1),w={href:"https://needle.tools",target:"_blank",rel:"noopener noreferrer"},_={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),P=e("li",null,[e("p",null,[e("strong",null,"Vite + SvelteKit")])],-1),x=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),A=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),W=e("strong",null,"Vercel & Nextjs",-1),T={href:"https://github.com/needle-engine/nextjs-sample",target:"_blank",rel:"noopener noreferrer"},E=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),V={class:"custom-container tip"},O=e("p",{class:"custom-container-title"},"TIP",-1),S=e("code",null,"needle.config.json",-1),I=e("code",null,"baseUrl",-1),R=t('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 easily creating a Progressive Web App (PWA) directly from our vite template. PWAs are web applications that load like regular web pages or websites but can offer user functionality such as working offline, push notifications, and device hardware access traditionally available only to native mobile applications.
By default, PWAs created with Needle have offline support, and can optionally refresh automatically when you publish a new version of your app.
',4),D={href:"https://vite-pwa-org.netlify.app/",target:"_blank",rel:"noopener noreferrer"},L=e("code",null,"npm install vite-plugin-pwa --save-dev",-1),M=e("li",null,[n("Modify "),e("code",null,"vite.config.js"),n(" as seen below. Make sure to pass the same "),e("code",null,"pwaOptions"),n(" object to both "),e("code",null,"needlePlugins"),n(" and "),e("code",null,"VitePWA"),n(".")],-1),z=t(`
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,{pwa: pwaOptions }),
+ VitePWA(pwaOptions),
+ ],
+ // the rest of your vite config...
+
All assets are cached by default
Note that by default, all assets in your build folder are added the PWA precache โ for large applications with many dynamic assets, this may not be what you want (imagine the YouTube PWA caching all videos once a user opens the app!). See More PWA Options for how to customize this behavior.
To test your PWA, deploy the page, for example using the DeployToFTP component. Then, open the deployed page in a browser and check if the PWA features work as expected:
`,4),U=e("li",null,"the app shows up as installable",-1),B=e("li",null,"the app works offline",-1),F={href:"https://pwabuilder.com/",target:"_blank",rel:"noopener noreferrer"},Y=t(`
PWAs use Service Workers to cache resources and provide offline support. Service Workers are somewhat harder to use during development, and typically are only enabled for builds (e.g. when you use a DeployTo... component).
You can enable PWA support for development by adding the following to the options object in your vite.config.js.
const pwaOptions ={
+ // Note: PWAs behave different in dev mode.
+ // Make sure to verify the behaviour in production builds!
+ devOptions:{
+ enabled:true,
+ }
+};
+
Please note that PWAs in development mode do not support offline usage โ trying it may result in unexpected behavior.
Websites typically show new or updated content on page refresh.
In some situations, you may want the page to refresh and reload automatically when a new version has been published โ such as in a museum, trade show, public display, or other long-running scenarios.
To enable automatic updates, set the updateInterval property in the pwaOptions object to a duration (in milliseconds) in which the app should check for updates. If an update is detected, the page will reload automatically.
`,9),H={class:"custom-container tip"},J=e("p",{class:"custom-container-title"},"Periodic Reloads and User Data",-1),K=e("br",null,null,-1),G={href:"https://vite-pwa-org.netlify.app/guide/prompt-for-update.html",target:"_blank",rel:"noopener noreferrer"},$=e("h3",{id:"more-pwa-options",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#more-pwa-options","aria-hidden":"true"},"#"),n(" More PWA options")],-1),Q={href:"https://vite-pwa-org.netlify.app/",target:"_blank",rel:"noopener noreferrer"},X=e("br",null,null,-1),Z=t(`
const pwaOptions ={
+ // manifest options provided here will override the defaults
+ manifest:{
+ name:"My App",
+ short_name:"My App",
+ theme_color:"#B2D464",
+ }
+};
+
For complex requirements like partial caching, custom service workers or different update strategies, you can remove the { pwa: pwaOptions } option from needlePlugins and add PWA functionality directly through the Vite PWA plugin.
# Accessing Needle Engine and Components from external javascript
`,3),ee=e("br",null,null,-1),ne=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),ae=e("em",null,"Loading Display",-1),se=t('
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),te=e("em",null,"Loading Display",-1),oe=e("p",null,[e("img",{src:p,alt:"custom loading"})],-1);function ie(le,pe){const s=l("ExternalLinkIcon"),o=l("RouterLink");return c(),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"),a(s)])]),v,e("p",null,[n("Needle Engine is unoponiated about the choice of framework. Our default template uses the popular "),e("a",b,[n("vite"),a(s)]),n(" as bundler. From there, you can add vue, svelte, nuxt, react, react-three-fiber or other frameworks, and we have samples for a lot of them. You can also integrate other bundlers, or use none at all โ just plain HTML and Javascript.")]),k,e("ul",null,[f,e("li",null,[e("p",null,[y,n(" โ This is what the "),e("a",w,[n("Needle Tools"),a(s)]),n(" website uses!. Find a sample to download "),e("a",_,[n("here"),a(s)]),n(".")])]),j,P,x,A,e("li",null,[e("p",null,[W,n(" โ Find a "),e("a",T,[n("example nextjs project here"),a(s)])])]),e("li",null,[e("p",null,[E,n(" โ Find a code example "),a(o,{to:"/vanilla-js.html"},{default:i(()=>[n("here")]),_:1})])])]),N,e("div",V,[O,e("p",null,[n("Some frameworks require custom settings in "),S,n(". Learn more "),a(o,{to:"/reference/needle-config-json.html"},{default:i(()=>[n("here")]),_:1}),n(". Typically, the "),I,n(" needs to be set.")])]),R,e("p",null,[n("Tree shaking refers to a common practice when it comes to bundling of web applications ("),e("a",q,[n("see MSDN docs"),a(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:")]),C,e("ol",null,[e("li",null,[n("Install the "),e("a",D,[n("Vite PWA plugin"),a(s)]),n(" in your web project: "),L]),M]),z,e("ul",null,[U,B,e("li",null,[n("the app is detected as offline-capable PWA by "),e("a",F,[n("pwabuilder.com"),a(s)])])]),Y,e("div",H,[J,e("p",null,[n("It's not recommended to use automatic reloads in applications where users are interacting with forms or other data that could be lost on a reload. For these applications, showing a reload prompt is recommended."),K,n(" See the "),e("a",G,[n("Vite PWA plugin documentation"),a(s)]),n(" for more information on how to implement a reload prompt instead of automatic reloading.")])]),$,e("p",null,[n("Since Needle uses the "),e("a",Q,[n("Vite PWA plugin"),a(s)]),n(" under the hood, you can use all options and hooks provided by that."),X,n(" For example, you can provide a partial manifest with a custom app title or theme color:")]),Z,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)."),ee,n(" For accessing components from regular javascript outside of the engine please refer to the "),a(o,{to:"/scripting.html#accessing-needle-engine-and-components-from-anywhere"},{default:i(()=>[n("interop with regular javascript section")]),_:1})]),ne,e("p",null,[n("See the "),ae,n(" section in "),a(o,{to:"/reference/needle-engine-attributes.html"},{default:i(()=>[n("needle engine component reference")]),_:1})]),se,e("p",null,[n("Please see the "),te,n(" section in "),a(o,{to:"/reference/needle-engine-attributes.html"},{default:i(()=>[n("needle engine component reference")]),_:1})]),oe])}const de=r(h,[["render",ie],["__file","html.html.vue"]]);export{de as default};
diff --git a/assets/html.html-c6d742ba.js b/assets/html.html-c6d742ba.js
new file mode 100644
index 000000000..9d3551c4c
--- /dev/null
+++ b/assets/html.html-c6d742ba.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":3,"title":"Testing PWAs","slug":"testing-pwas","link":"#testing-pwas","children":[]},{"level":3,"title":"Automatically update running apps","slug":"automatically-update-running-apps","link":"#automatically-update-running-apps","children":[]},{"level":3,"title":"More PWA options","slug":"more-pwa-options","link":"#more-pwa-options","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":{"updatedTime":1725399379000},"filePathRelative":"html.md"}');export{e as data};
diff --git a/assets/index.html-00a074af.js b/assets/index.html-00a074af.js
new file mode 100644
index 000000000..d5d4b1ab6
--- /dev/null
+++ b/assets/index.html-00a074af.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-f6820c83.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-013f9a89.js b/assets/index.html-013f9a89.js
new file mode 100644
index 000000000..dc636a5a4
--- /dev/null
+++ b/assets/index.html-013f9a89.js
@@ -0,0 +1,61 @@
+import{_ as l,M as o,p,q as i,N as a,V as t,R as n,t as s}from"./framework-f6820c83.js";const u={},r=n("p",null,"I combined two checks - Needle's check to see if it's a mobile device (this way, you can exclude desktops), and then a second check that uses user agent info to see if it's one of the most common mobile systems. Result: the button does not appear on mobile phones, but it is visible on Quest and Pico ๐",-1),k=n("p",null,"P.S: I am using Svelte for 2D UI handling.",-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(" isMobileDevice"),n("span",{class:"token punctuation"},","),s(" NeedleXRSession "),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 operator"},"..."),s(`
+
+`),n("span",{class:"token comment"},"// check if this is a mobile phone"),s(`
+`),n("span",{class:"token keyword"},"function"),s(),n("span",{class:"token function"},"isMobilePhone"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"return"),s(),n("span",{class:"token regex"},[n("span",{class:"token regex-delimiter"},"/"),n("span",{class:"token regex-source language-regex"},"Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini"),n("span",{class:"token regex-delimiter"},"/"),n("span",{class:"token regex-flags"},"i")]),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"test"),n("span",{class:"token punctuation"},"("),s("navigator"),n("span",{class:"token punctuation"},"."),s("userAgent"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+`),n("span",{class:"token punctuation"},"}"),s(`
+
+`),n("span",{class:"token operator"},"..."),s(`
+
+`),n("span",{class:"token comment"},"// show the button, if the device is not a mobile phone and VR is supported"),s(`
+`),n("span",{class:"token punctuation"},"{"),s("#"),n("span",{class:"token keyword"},"if"),s(" "),n("span",{class:"token function"},"isMobileDevice"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"&&"),s(),n("span",{class:"token operator"},"!"),n("span",{class:"token function"},"isMobilePhone"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"&&"),s(" $haveVR "),n("span",{class:"token punctuation"},"}"),s(`
+ `),n("span",{class:"token operator"},"<"),s("VrButton buttonFunction"),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 operator"},"=>"),s(),n("span",{class:"token function"},"StartVR"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token operator"},"/"),n("span",{class:"token operator"},">"),s(`
+`),n("span",{class:"token punctuation"},"{"),n("span",{class:"token operator"},"/"),n("span",{class:"token keyword"},"if"),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),b=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),m=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),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(" 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 f(h,y){const e=o("contribution-preview"),c=o("contributions-author");return p(),i("div",null,[a(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:t(()=>[a(e,{title:"Custom VR Button, that appears only on headsets and not on mobile phones",pageUrl:"/docs/community/contributions/llllkatjallll/custom-vr-button-that-appears-only-on-headsets-and-not-on-mobile-phones"},{default:t(()=>[r,k,d]),_:1}),a(e,{title:"Set fallback material for USDZ exporter",pageUrl:"/docs/community/contributions/llllkatjallll/set-fallback-material-for-usdz-exporter"},{default:t(()=>[b,m,v]),_:1})]),_:1})])}const x=l(u,[["render",f],["__file","index.html.vue"]]);export{x 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-0f3c5ccd.js b/assets/index.html-0f3c5ccd.js
new file mode 100644
index 000000000..744d2a3a0
--- /dev/null
+++ b/assets/index.html-0f3c5ccd.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-165956bc","path":"/community/contributions/llllkatjallll/custom-vr-button-that-appears-only-on-headsets-and-not-on-mobile-phones/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/llllkatjallll: custom vr button that appears only on headsets and not on mobile phones.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-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-1c180a2f.js b/assets/index.html-1c180a2f.js
new file mode 100644
index 000000000..6bec151bb
--- /dev/null
+++ b/assets/index.html-1c180a2f.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-f6820c83.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-238978c8.js b/assets/index.html-238978c8.js
new file mode 100644
index 000000000..5030a9e1a
--- /dev/null
+++ b/assets/index.html-238978c8.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 e}from"./framework-f6820c83.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,k){const a=r("ExternalLinkIcon"),o=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:e(()=>[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:e(()=>[t(o,{title:"Custom VR Button, that appears only on headsets and not on mobile phones",url:"/docs/community/contributions/llllkatjallll/custom-vr-button-that-appears-only-on-headsets-and-not-on-mobile-phones"}),t(o,{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/ROBYER1",author:"ROBYER1",page:"/docs/community/contributions/robyer1",profileImage:"https://avatars.githubusercontent.com/u/10745594?s=100&u=daf2c8b5dad729e556ae2a01c721672b24bc108a&v=4"},{default:e(()=>[t(o,{title:"Microphone access in a browser window (and streamed playback)",url:"/docs/community/contributions/robyer1/microphone-access-in-a-browser-window-and-streamed-playback"}),t(o,{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/ericcraft-mh",author:"ericcraft-mh",page:"/docs/community/contributions/ericcraft-mh",profileImage:"https://avatars.githubusercontent.com/u/99364056?s=100&v=4"},{default:e(()=>[t(o,{title:"QuickLook Vertical Image Tracker",url:"/docs/community/contributions/ericcraft-mh/quicklook-vertical-image-tracker"})]),_: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:e(()=>[t(o,{title:"Always open in specific browser",url:"/docs/community/contributions/krisrok/always-open-in-specific-browser"})]),_: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:e(()=>[t(o,{title:"Camera Video Background",url:"/docs/community/contributions/marwie/camera-video-background"}),t(o,{title:"USDZ: Hide Object on Start",url:"/docs/community/contributions/marwie/usdz-hide-object-on-start"}),t(o,{title:"Everywhere Action: Emphasize on Click",url:"/docs/community/contributions/marwie/everywhere-action-emphasize-on-click"}),t(o,{title:"Control a Timeline by scroll",url:"/docs/community/contributions/marwie/control-a-timeline-by-scroll"}),t(o,{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:e(()=>[t(o,{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:e(()=>[t(o,{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(o,{title:"Squeeze to Scale (Object or World) in VR",url:"/docs/community/contributions/web3kev/squeeze-to-scale-object-or-world-in-vr"}),t(o,{title:"Network instantiation of multiple objects",url:"/docs/community/contributions/web3kev/network-instantiation-of-multiple-objects"})]),_:1})]),_:1})])}const y=l(b,[["render",h],["__file","index.html.vue"]]);export{y as default};
diff --git a/assets/index.html-2c69ad93.js b/assets/index.html-2c69ad93.js
new file mode 100644
index 000000000..ed24344ea
--- /dev/null
+++ b/assets/index.html-2c69ad93.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-f6820c83.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-32447045.js b/assets/index.html-32447045.js
new file mode 100644
index 000000000..1cdaa57e1
--- /dev/null
+++ b/assets/index.html-32447045.js
@@ -0,0 +1,66 @@
+import{_ as p,M as e,p as l,q as u,N as a,V as t,R as n,t as s}from"./framework-f6820c83.js";const i={},r=n("p",null,"A simple script to show how to request access to, then access a microphone device and also play back the audio stream to debug it. A useful starting point for making an experience revolving around microphone access.",-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(),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"},"Microphone"),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 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"},"getLocalStream"),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"},"public"),s(" el"),n("span",{class:"token operator"},":"),s(" Element"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token function"},"attachStream"),n("span",{class:"token punctuation"},"("),s("stream"),n("span",{class:"token punctuation"},","),s(" el"),n("span",{class:"token punctuation"},","),s(" options"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"var"),s(" item"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"var"),s(),n("span",{class:"token constant"},"URL"),s(),n("span",{class:"token operator"},"="),s(" window"),n("span",{class:"token punctuation"},"."),n("span",{class:"token constant"},"URL"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"var"),s(" element "),n("span",{class:"token operator"},"="),s(" el"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"var"),s(" opts "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"{"),s(`
+ autoplay`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},","),s(`
+ mirror`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},","),s(`
+ muted`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},","),s(`
+ audio`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},","),s(`
+ disableContextMenu`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token boolean"},"false"),n("span",{class:"token punctuation"},","),s(`
+ `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},";"),s(`
+
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),s("options"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"for"),s(),n("span",{class:"token punctuation"},"("),s("item "),n("span",{class:"token keyword"},"in"),s(" options"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ opts`),n("span",{class:"token punctuation"},"["),s("item"),n("span",{class:"token punctuation"},"]"),s(),n("span",{class:"token operator"},"="),s(" options"),n("span",{class:"token punctuation"},"["),s("item"),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"},"if"),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token operator"},"!"),s("element"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ element `),n("span",{class:"token operator"},"="),s(" document"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"createElement"),n("span",{class:"token punctuation"},"("),s("opts"),n("span",{class:"token punctuation"},"."),s("audio "),n("span",{class:"token operator"},"?"),s(),n("span",{class:"token string"},'"audio"'),s(),n("span",{class:"token operator"},":"),s(),n("span",{class:"token string"},'"video"'),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 keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),s("element"),n("span",{class:"token punctuation"},"."),s("tagName"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"toLowerCase"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"==="),s(),n("span",{class:"token string"},'"audio"'),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ opts`),n("span",{class:"token punctuation"},"."),s("audio "),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 keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),s("opts"),n("span",{class:"token punctuation"},"."),s("autoplay"),n("span",{class:"token punctuation"},")"),s(" element"),n("span",{class:"token punctuation"},"."),s("autoplay "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token string"},'"autoplay"'),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),s("opts"),n("span",{class:"token punctuation"},"."),s("muted"),n("span",{class:"token punctuation"},")"),s(" element"),n("span",{class:"token punctuation"},"."),s("muted "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token operator"},"!"),s("opts"),n("span",{class:"token punctuation"},"."),s("audio "),n("span",{class:"token operator"},"&&"),s(" opts"),n("span",{class:"token punctuation"},"."),s("mirror"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'""'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token string"},'"moz"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token string"},'"webkit"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token string"},'"o"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token string"},'"ms"'),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"forEach"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"function"),s(),n("span",{class:"token punctuation"},"("),s("prefix"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"var"),s(" styleName "),n("span",{class:"token operator"},"="),s(" prefix "),n("span",{class:"token operator"},"?"),s(" prefix "),n("span",{class:"token operator"},"+"),s(),n("span",{class:"token string"},'"Transform"'),s(),n("span",{class:"token operator"},":"),s(),n("span",{class:"token string"},'"transform"'),n("span",{class:"token punctuation"},";"),s(`
+ element`),n("span",{class:"token punctuation"},"."),s("style"),n("span",{class:"token punctuation"},"["),s("styleName"),n("span",{class:"token punctuation"},"]"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token string"},'"scaleX(-1)"'),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(`
+
+ element`),n("span",{class:"token punctuation"},"."),s("srcObject "),n("span",{class:"token operator"},"="),s(" stream"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"return"),s(" element"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token punctuation"},"}"),s(`
+
+ `),n("span",{class:"token function"},"getLocalStream"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),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"},"false"),n("span",{class:"token punctuation"},","),s(`
+ audio`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},","),s(`
+ `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),s(`
+ `),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"then"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},"("),s("stream"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token operator"},"=>"),s(),n("span",{class:"token punctuation"},"{"),s(`
+ `),n("span",{class:"token keyword"},"var"),s(" doesnotexist "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token operator"},"!"),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("el"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("el "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"attachStream"),n("span",{class:"token punctuation"},"("),s("stream"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("el"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token punctuation"},"{"),s(`
+ audio`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token boolean"},"true"),n("span",{class:"token punctuation"},","),s(`
+ autoplay`),n("span",{class:"token operator"},":"),s(),n("span",{class:"token boolean"},"true"),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"},"if"),s(),n("span",{class:"token punctuation"},"("),s("doesnotexist"),n("span",{class:"token punctuation"},")"),s(" document"),n("span",{class:"token punctuation"},"."),s("body"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"appendChild"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("el"),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 punctuation"},"."),n("span",{class:"token function"},"catch"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},"("),s("err"),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"},'"u got an error:"'),s(),n("span",{class:"token operator"},"+"),s(" err"),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 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"})])],-1),d=n("p",null,"This is live for preview over at https://needle-ar.glitch.me/",-1),m=n("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),b=n("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),v=n("p",null,"Public Github Repository: https://github.com/ROBYER1/Needle-AR-Demo",-1);function h(f,w){const o=e("contribution-preview"),c=e("contributions-author");return l(),u("div",null,[a(c,{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:t(()=>[a(o,{title:"Microphone access in a browser window (and streamed playback)",pageUrl:"/docs/community/contributions/robyer1/microphone-access-in-a-browser-window-and-streamed-playback"},{default:t(()=>[r,k]),_:1}),a(o,{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:t(()=>[d,m,b,v]),_:1})]),_:1})])}const g=p(i,[["render",h],["__file","index.html.vue"]]);export{g as default};
diff --git a/assets/index.html-3604b913.js b/assets/index.html-3604b913.js
new file mode 100644
index 000000000..46613a7b6
--- /dev/null
+++ b/assets/index.html-3604b913.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-f6820c83.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-3b58543e.js b/assets/index.html-3b58543e.js
new file mode 100644
index 000000000..11694cf83
--- /dev/null
+++ b/assets/index.html-3b58543e.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-f6820c83.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-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-4556c585.js b/assets/index.html-4556c585.js
new file mode 100644
index 000000000..3978c2af7
--- /dev/null
+++ b/assets/index.html-4556c585.js
@@ -0,0 +1,37 @@
+import{_ as p}from"./logo-b695892f.js";import{_,M as i,p as m,q as g,Q as r,N as n,V as o,R as e,t,a1 as a}from"./framework-f6820c83.js";const b="/docs/imgs/unity-logo.webp",w="/docs/imgs/threejs-logo.webp",y={},f=a('
With Needle Engine, you can create fully interactive 3D websites. They can be deployed anywhere on the web and get optimized automatically by the Needle Engine Build Pipeline.
Needle Engine is available as a download for Unity, for Blender, and for web projects without an editor integration.
',4),v=e("br",null,null,-1),x=e("br",null,null,-1),k=e("img",{src:b,style:{"max-height":"70px"}},null,-1),S=e("h2",{id:"needle-engine-for-unity",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#needle-engine-for-unity","aria-hidden":"true"},"#"),t(" Needle Engine for Unity")],-1),j=e("p",null,[e("em",null,"Supported Unity versions: 2021.3 LTS, 2022.3 LTS")],-1),N=e("strong",null,"Download Needle Engine for Unity",-1),E={class:"custom-container details"},T=e("summary",null,"Next Steps",-1),C=e("li",null,[e("p",null,"Drop the downloaded .unitypackage file into a Unity project and confirm that you want to import it.")],-1),I=e("li",null,[e("p",null,'Wait a moment 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. You can safely close that window.')],-1),B=e("strong",null,"Explore Samples",-1),D=e("em",null,"Needle Engine > Explore Samples",-1),q={href:"https://engine.needle.tools/samples",target:"_blank",rel:"noopener noreferrer"},U={class:"custom-container details"},P=e("summary",null,"Video Tutorial: Starting from a fresh Unity project",-1),O=e("hr",null,null,-1),F={class:"custom-container details"},W=e("summary",null,"Create a new scene from a Scene Template",-1),L=e("p",null,[t("We provide a number of Scene Templates for quickly starting new projects."),e("br"),t(" These allow you to go from idea to prototype in a few clicks.")],-1),M=e("li",null,[e("p",null,[t("Click on "),e("code",null,"File > New Scene")])],-1),V=e("code",null,"Create",-1),A=e("br",null,null,-1),R={href:"https://engine.needle.tools/samples/collaborative-sandbox",target:"_blank",rel:"noopener noreferrer"},z=e("li",null,[e("p",null,"Click Play to install and startup your new web project.")],-1),Y=e("p",null,[e("img",{src:"https://user-images.githubusercontent.com/2693840/185917275-a147cd90-d515-4086-950d-78358185b1ef.png",alt:"20220822-140539-wqvW-Unity_oC0z-needle"})],-1),G={class:"custom-container details"},Q=a('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),H={start:"3"},K=e("strong",null,"Choose a web project template",-1),X={href:"https://vitejs.dev/",target:"_blank",rel:"noopener noreferrer"},J=e("li",null,[e("p",null,"Click Play to install and start your new web project")],-1),Z=e("br",null,null,-1),$=e("br",null,null,-1),ee=e("br",null,null,-1),te=e("img",{src:p,style:{"max-height":"70px"}},null,-1),ne=e("h2",{id:"needle-engine-for-blender",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#needle-engine-for-blender","aria-hidden":"true"},"#"),t(" Needle Engine for Blender")],-1),oe=e("p",null,[e("em",null,"Supported Blender versions: 4.0+")],-1),le=e("strong",null,"Download Needle Engine for Blender",-1),se=e("br",null,null,-1),ie=e("ul",null,[e("li",null,"The Blender add-on 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),re=e("br",null,null,-1),ae=e("br",null,null,-1),de=e("br",null,null,-1),ce=e("img",{src:w,style:{"max-height":"70px"}},null,-1),ue=e("h2",{id:"needle-engine-without-editor-integration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#needle-engine-without-editor-integration","aria-hidden":"true"},"#"),t(" Needle Engine without Editor Integration")],-1),he={href:"https://threejs.org/",target:"_blank",rel:"noopener noreferrer"},pe=e("p",null,[t("You can install Needle Engine from "),e("code",null,"npm"),t(" by running:"),e("br"),e("br"),e("code",null,"npm i @needle-tools/engine")],-1),_e={href:"https://stackblitz.com/",target:"_blank",rel:"noopener noreferrer"},me=e("br",null,null,-1),ge={href:"https://engine.needle.tools/new",target:"_blank",rel:"noopener noreferrer"},be=a('
If you're not using npm or a bundler, you can instead add a prebundled version of Needle Engine to your website:
https://unpkg.com/@needle-tools/engine/dist/needle-engine.min.js or https://unpkg.com/@needle-tools/engine/dist/needle-engine.light.min.js(no physics module, smaller)
Needle Engine uses Node.js to manage, preview and build the web app that you are creating locally on your computer. It is also used for uploading (deploying) your website to the internet. Please download it from the official 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.
',3),Ce=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),Ie=e("br",null,null,-1),Be={href:"https://discord.needle.tools",target:"_blank",rel:"noopener noreferrer"},De={href:"https://forum.needle.tools",target:"_blank",rel:"noopener noreferrer"};function qe(Ue,Pe){const c=i("needle-button"),s=i("ExternalLinkIcon"),u=i("video-embed"),l=i("RouterLink"),d=i("os-link"),h=i("ClientOnly");return m(),g("div",null,[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 `),v,x,k,S,j,n(c,{event_goal:"download_unity",event_position:"getting_started",large:"",href:"https://engine.needle.tools/downloads/unity?utm_source=needle_docs&utm_content=getting_started"},{default:o(()=>[N]),_: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("details",E,[T,e("ul",null,[C,I,e("li",null,[e("p",null,[B,t(" โ Select the menu option "),D,t(" to view, open and modify all available "),e("a",q,[t("sample scenes"),n(s)]),t(".")])])])]),e("details",U,[P,n(u,{src:"https://www.youtube.com/watch?v=3dB-d1Jo_Mk",limit_height:""}),n(u,{src:"https://www.youtube.com/watch?v=gZX_sqrne8U",limit_height:""})]),O,e("details",F,[W,L,e("ol",null,[M,e("li",null,[e("p",null,[t("Select one of the templates with (needle) in their name and click "),V,t("."),A,t(" We recommend the "),e("a",R,[t("Collaborative Sandbox"),n(s)]),t(" template which is a great way to get started with interactivity, multiplayer, and adding assets.")])]),z]),Y]),e("details",G,[Q,e("ol",H,[e("li",null,[e("p",null,[K,t(" Now, select a web project template for your project. The default template is based on "),e("a",X,[t("Vite"),n(s)]),t(", a fast web app bundler.")])]),J])]),Z,$,ee,te,ne,oe,n(c,{event_goal:"download_blender",event_position:"getting_started",large:"",href:"https://engine.needle.tools/downloads/blender?utm_source=needle_docs&utm_content=getting_started"},{default:o(()=>[le]),_:1}),t(),se,ie,e("p",null,[e("strong",null,[t("See "),n(l,{to:"/blender/"},{default:o(()=>[t("Needle Engine for Blender")]),_:1})]),t(" for a full list of features and instructions on getting started.")]),re,ae,de,ce,ue,e("p",null,[t("You can work directly with Needle Engine without using any Integration. Needle Engine uses "),e("a",he,[t("three.js"),n(s)]),t(" as scene graph and rendering backend, so all functionality from three.js is available in Needle as well.")]),pe,e("p",null,[t("For quick experiments, we provide a convenient link to create a new project ready to start, powered by "),e("a",_e,[t("StackBlitz"),n(s)]),t(":"),me,e("a",ge,[t("engine.needle.tools/new"),n(s)])]),be,n(h,null,{default:o(()=>[r(' Needle Engine for Unity โ Needle Engine for Blender '),n(d,{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: Node.js 18+ โญ")]),_:1}),we,t(),ye,fe,ve,e("p",null,[xe,n(d,{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 texture tools ๐")]),_:1}),ke,t(" We use toktx to locally optimize your files. Learn more about production builds "),n(l,{to:"/deployment.html#production-builds"},{default:o(()=>[t("in the docs")]),_:1}),t(".")]),e("p",null,[Se,n(d,{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: Visual Studio Code ๐")]),_:1}),je,t(" When you plan to edit or write code (js or HTML) then we "),Ne,t(" that you use VSCode as your code editor.")]),Ee]),_:1}),Te,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.
+`),Ce,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})]),e("li",null,[n(l,{to:"/deployment.html"},{default:o(()=>[t("Deploy your website to the web")]),_:1})]),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."),Ie,t(" You can also join our "),e("a",Be,[t("Discord Community"),n(s)]),t(" or "),e("a",De,[t("Forum"),n(s)])])])}const We=_(y,[["render",qe],["__file","index.html.vue"]]);export{We 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-45bf7509.js b/assets/index.html-45bf7509.js
new file mode 100644
index 000000000..306322c4c
--- /dev/null
+++ b/assets/index.html-45bf7509.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-b5125a16","path":"/community/contributions/ericcraft-mh/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/contributions: ericcraft mh.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-4d11b3c8.js b/assets/index.html-4d11b3c8.js
new file mode 100644
index 000000000..c13b51fe6
--- /dev/null
+++ b/assets/index.html-4d11b3c8.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-f6820c83.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-4f00c2ff.js b/assets/index.html-4f00c2ff.js
new file mode 100644
index 000000000..369942922
--- /dev/null
+++ b/assets/index.html-4f00c2ff.js
@@ -0,0 +1,44 @@
+import{_ as l,M as e,p as i,q as u,N as t,V as o,R as n,t as s}from"./framework-f6820c83.js";const r={},k={href:"https://developer.apple.com/documentation/arkit/arkit_in_ios/content_anchors/detecting_images_in_an_ar_experience",target:"_blank",rel:"noopener noreferrer"},d=n("blockquote",null,[n("p",null,[n("code",null,"SCNPlane"),s(" is vertically oriented in its local coordinate space, but "),n("code",null,"ARImageAnchor"),s(" assumes the image is horizontal in its local space, so rotate the plane to match.")])],-1),m=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(" USDZExporter "),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(" Euler "),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"},"QuickLookObjectsToFix"),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(`
+ objectToFix`),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(" usdzExporter"),n("span",{class:"token operator"},"!"),n("span",{class:"token operator"},":"),s(" USDZExporter"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"private"),s(" startRot"),n("span",{class:"token operator"},":"),s(" Euler "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Euler"),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"},")"),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"},"."),s("startRot "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("objectToFix"),n("span",{class:"token punctuation"},"."),s("rotation"),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 keyword"},"private"),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 keyword"},"this"),n("span",{class:"token punctuation"},"."),s("objectToFix"),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"},"this"),n("span",{class:"token punctuation"},"."),s("objectToFix"),n("span",{class:"token punctuation"},"."),s("rotation"),n("span",{class:"token punctuation"},"."),s("x "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token operator"},"-"),s("Math"),n("span",{class:"token punctuation"},"."),n("span",{class:"token constant"},"PI"),s(),n("span",{class:"token operator"},"/"),s(),n("span",{class:"token number"},"2"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("objectToFix"),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 punctuation"},"}"),s(`
+
+ `),n("span",{class:"token keyword"},"private"),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 keyword"},"this"),n("span",{class:"token punctuation"},"."),s("objectToFix"),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"},"this"),n("span",{class:"token punctuation"},"."),s("objectToFix"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"setRotationFromEuler"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("startRot"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(`
+ `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),s("objectToFix"),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 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"})])],-1),b={href:"https://github.com/llllkatjallll",target:"_blank",rel:"noopener noreferrer"},v={href:"https://github.com/needle-tools/needle-engine-support/discussions/184",target:"_blank",rel:"noopener noreferrer"},h=n("p",null,"EDIT: Code cleanup and fixes.",-1);function f(x,_){const a=e("ExternalLinkIcon"),c=e("contribution-preview"),p=e("contributions-author");return i(),u("div",null,[t(p,{overviewLink:"/docs/community/contributions",name:"ericcraft-mh",url:"https://github.com/ericcraft-mh",profileImage:"https://avatars.githubusercontent.com/u/99364056?s=100&v=4",githubUrl:"https://github.com/ericcraft-mh"},{default:o(()=>[t(c,{title:"QuickLook Vertical Image Tracker",pageUrl:"/docs/community/contributions/ericcraft-mh/quicklook-vertical-image-tracker"},{default:o(()=>[n("p",null,[s("In cases in which you are using QuickLook Image Tracker and Vertical Imagery you will need to correct the orientation of the model. As noted on the "),n("a",k,[s("Detecting Images in an AR Experience"),t(a)]),s(" page:")]),d,m,n("p",null,[s("Thanks to "),n("a",b,[s("llllkatjallll"),t(a)]),s(" as their "),n("a",v,[s("Set fallback material for USDZ exporter"),t(a)]),s(" solution helped me come up with a working solution for this.")]),h]),_:1})]),_:1})])}const y=l(r,[["render",f],["__file","index.html.vue"]]);export{y as default};
diff --git a/assets/index.html-57d757de.js b/assets/index.html-57d757de.js
new file mode 100644
index 000000000..2421b4230
--- /dev/null
+++ b/assets/index.html-57d757de.js
@@ -0,0 +1,44 @@
+import{_ as p,M as e,p as c,q as i,N as a,R as s,t as n,a1 as l}from"./framework-f6820c83.js";const u={},r=s("p",null,[s("a",{href:"/docs/community/contributions"},"Overview")],-1),k={href:"https://developer.apple.com/documentation/arkit/arkit_in_ios/content_anchors/detecting_images_in_an_ar_experience",target:"_blank",rel:"noopener noreferrer"},d=l(`
SCNPlane is vertically oriented in its local coordinate space, but ARImageAnchor assumes the image is horizontal in its local space, so rotate the plane to match.
`,2),v={href:"https://github.com/llllkatjallll",target:"_blank",rel:"noopener noreferrer"},m={href:"https://github.com/needle-tools/needle-engine-support/discussions/184",target:"_blank",rel:"noopener noreferrer"},b=s("p",null,"EDIT: Code cleanup and fixes.",-1);function h(f,x){const o=e("contribution-header"),t=e("ExternalLinkIcon");return c(),i("div",null,[r,a(o,{url:"https://github.com/ericcraft-mh",author:"ericcraft-mh",page:"/docs/community/contributions/ericcraft-mh",profileImage:"https://avatars.githubusercontent.com/u/99364056?s=100&v=4",githubUrl:"https://github.com/needle-tools/needle-engine-support/discussions/191",title:"QuickLook Vertical Image Tracker",gradient:"True"}),s("p",null,[n("In cases in which you are using QuickLook Image Tracker and Vertical Imagery you will need to correct the orientation of the model. As noted on the "),s("a",k,[n("Detecting Images in an AR Experience"),a(t)]),n(" page:")]),d,s("p",null,[n("Thanks to "),s("a",v,[n("llllkatjallll"),a(t)]),n(" as their "),s("a",m,[n("Set fallback material for USDZ exporter"),a(t)]),n(" solution helped me come up with a working solution for this.")]),b])}const w=p(u,[["render",h],["__file","index.html.vue"]]);export{w 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-64bc0c3c.js b/assets/index.html-64bc0c3c.js
new file mode 100644
index 000000000..a59e5c7d6
--- /dev/null
+++ b/assets/index.html-64bc0c3c.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-1d2bf24a","path":"/community/contributions/ericcraft-mh/quicklook-vertical-image-tracker/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/ericcraft mh: quicklook vertical image tracker.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-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-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-7517d60a.js b/assets/index.html-7517d60a.js
new file mode 100644
index 000000000..f932ab77c
--- /dev/null
+++ b/assets/index.html-7517d60a.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-f6820c83.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-75e422fa.js b/assets/index.html-75e422fa.js
new file mode 100644
index 000000000..15f390d66
--- /dev/null
+++ b/assets/index.html-75e422fa.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-44471890","path":"/community/contributions/robyer1/microphone-access-in-a-browser-window-and-streamed-playback/","title":"","lang":"en-US","frontmatter":{"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/robyer1: microphone access in a browser window and streamed playback.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-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-7afee307.js b/assets/index.html-7afee307.js
new file mode 100644
index 000000000..cdf0ef5f2
--- /dev/null
+++ b/assets/index.html-7afee307.js
@@ -0,0 +1,66 @@
+import{_ as a,M as t,p,q as o,N as e,R as n,a1 as c}from"./framework-f6820c83.js";const i={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=c(`
A simple script to show how to request access to, then access a microphone device and also play back the audio stream to debug it. A useful starting point for making an experience revolving around microphone access.
`,2);function k(r,d){const s=t("contribution-header");return p(),o("div",null,[l,e(s,{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/192",title:"Microphone access in a browser window (and streamed playback)",gradient:"True"}),u])}const m=a(i,[["render",k],["__file","index.html.vue"]]);export{m as default};
diff --git a/assets/index.html-7de73919.js b/assets/index.html-7de73919.js
new file mode 100644
index 000000000..9593e932c
--- /dev/null
+++ b/assets/index.html-7de73919.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-8daa1a0e","path":"/","title":"","lang":"en-US","frontmatter":{"next":"./getting-started/index.md","sidebar":false,"editLink":false,"lastUpdated":false,"footer":"Copyright ยฉ 2024 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. Work on your machine and deploy anywhere. Needle Engine is flexible, extensible and has built-in support for collaboration and XR. It is built around the glTF standard for 3D assets.\\nPowerful integrations for Unity and Blender allow artists and developers to collaborate and manage web applications inside battle-tested 3d editors. These Integrations allow you to use editor features for creating models, authoring materials, animating and sequencing animations, baking lightmaps and more with ease.\\nOur 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. Work on your machine and deploy anywhere. Needle Engine is flexible, extensible and has built-in support for collaboration and XR. It is built around the glTF standard for 3D assets.\\nPowerful integrations for Unity and Blender allow artists and developers to collaborate and manage web applications inside battle-tested 3d editors. These Integrations allow you to use editor features for creating models, authoring materials, animating and sequencing animations, baking lightmaps and more with ease.\\nOur powerful compression and optimization pipeline for the web make sure your files are ready, small and load fast."},"headers":[],"git":{"updatedTime":1724764801000},"filePathRelative":"index.md"}');export{e as data};
diff --git a/assets/index.html-7dfa6204.js b/assets/index.html-7dfa6204.js
new file mode 100644
index 000000000..a654a7b7a
--- /dev/null
+++ b/assets/index.html-7dfa6204.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-f6820c83.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-81a91362.js b/assets/index.html-81a91362.js
new file mode 100644
index 000000000..e1929902b
--- /dev/null
+++ b/assets/index.html-81a91362.js
@@ -0,0 +1,16 @@
+import{_ as a,M as e,p as t,q as o,N as p,R as n,a1 as i}from"./framework-f6820c83.js";const c={},l=n("p",null,[n("a",{href:"/docs/community/contributions"},"Overview")],-1),u=i(`
I combined two checks - Needle's check to see if it's a mobile device (this way, you can exclude desktops), and then a second check that uses user agent info to see if it's one of the most common mobile systems. Result: the button does not appear on mobile phones, but it is visible on Quest and Pico ๐
P.S: I am using Svelte for 2D UI handling.
import{ isMobileDevice, NeedleXRSession }from"@needle-tools/engine";
+
+...
+
+// check if this is a mobile phone
+functionisMobilePhone(){
+ return/Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+}
+
+...
+
+// show the button, if the device is not a mobile phone and VR is supported
+{#ifisMobileDevice()&&!isMobilePhone()&& $haveVR }
+ <VrButton buttonFunction={()=>StartVR()}/>
+{/if}
+
`,3);function r(d,k){const s=e("contribution-header");return t(),o("div",null,[l,p(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/198",title:"Custom VR Button, that appears only on headsets and not on mobile phones",gradient:"True"}),u])}const v=a(c,[["render",r],["__file","index.html.vue"]]);export{v as default};
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-915af730.js b/assets/index.html-915af730.js
new file mode 100644
index 000000000..1c61e8155
--- /dev/null
+++ b/assets/index.html-915af730.js
@@ -0,0 +1 @@
+import{_ as o,M as n,p as a,q as r,N as i,R as e}from"./framework-f6820c83.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-932f723a.js b/assets/index.html-932f723a.js
new file mode 100644
index 000000000..e0ddb8c1c
--- /dev/null
+++ b/assets/index.html-932f723a.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-f6820c83.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-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-ac7d998f.js b/assets/index.html-ac7d998f.js
new file mode 100644
index 000000000..5d1172346
--- /dev/null
+++ b/assets/index.html-ac7d998f.js
@@ -0,0 +1,36 @@
+import{_ as u}from"./logo-b695892f.js";import{_ as h}from"./texture-compression-8cd31165.js";import{_ as m,M as r,p as b,q as g,N as t,R as e,V as a,t as n,a1 as i}from"./framework-f6820c83.js";const k="/docs/blender/settings.webp",_="/docs/blender/project-panel.webp",w="/docs/blender/project-panel-2.webp",f="/docs/blender/project-panel-3.webp",v="/docs/blender/settings-color-management.webp",y="/docs/blender/environment.webp",x="/docs/blender/environment-camera.webp",j="/docs/blender/dont-export.webp",C="/docs/blender/animatorcontroller-open.webp",T="/docs/blender/animatorcontroller-overview.webp",B="/docs/blender/animatorcontroller-assigning.webp",A="/docs/blender/timeline_setup.webp",P="/docs/blender/timeline.webp",S="/docs/blender/components-panel.webp",N="/docs/blender/components-panel-select.webp",E="/docs/blender/remove-component.webp",I="/docs/blender/lightmapping-object.webp",R="/docs/blender/lightmapping-scene-panel.webp",D="/docs/blender/lightmapping-panel.webp",L="/docs/blender/updates.webp",Y="/docs/blender/bugreporter.webp",q={},M=e("br",null,null,-1),U=e("img",{src:u,style:{"max-height":"70px"}},null,-1),V=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),W=e("p",null,[n("Thank you for using Needle Engine for Blender."),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),z=e("p",null,[e("em",null,"Automatically export HDRI environment lights directly from blender. Save to reload your local server")],-1),F=e("h1",{id:"content-overview",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#content-overview","aria-hidden":"true"},"#"),n(" Content Overview")],-1),O={class:"table-of-contents"},G=e("a",{target:"_blank",href:"https://www.blender.org/download/"},"Install Blender",-1),H=e("a",{class:"plausible-event-name=download_blender plausible-event-position=blender_download",target:"_blank",href:"https://engine.needle.tools/downloads/blender?utm_source=needle_docs&utm_content=blender"},"Download Needle Engine for Blender",-1),X=e("h2",{id:"preface",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#preface","aria-hidden":"true"},"#"),n(" Preface")],-1),J=e("strong",null,"Your feedback is invaluable",-1),$=e("br",null,null,-1),K={href:"https://forum.needle.tools",target:"_blank",rel:"noopener noreferrer"},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
You can change the environment lighting and skybox using the Viewport shading options. Assign a cubemap to use for lighting or the background skybox. You can adjust the strength or blur to modify the appearance to your liking.
Note: To also see the skybox cubemap in the browser increase the World Opacity to 1.
Note: Alternatively you can enable the Scene World setting in the Viewport Shading tab to use the environment texture assigned in the blender world settings.
',16),te=e("p",null,[n("Alternatively if you don't want to see the cubemap as a background add a Camera component to your Blender Camera and change "),e("code",null,"clearFlags: SolidColor"),n(" - note that the Camera "),e("code",null,"backgroundBlurriness"),n(" and "),e("code",null,"backgroundIntensity"),n(" settings override the Viewport shading settings.")],-1),ae=e("p",null,[e("img",{src:x,alt:"Environment Camera"})],-1),oe=e("h3",{id:"add-your-custom-hdri-exr-environment-lighting-and-skybox",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#add-your-custom-hdri-exr-environment-lighting-and-skybox","aria-hidden":"true"},"#"),n(" Add your custom HDRi / EXR environment lighting and skybox")],-1),se=i('
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)
',5),ie=e("h3",{id:"animatorcontroller",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#animatorcontroller","aria-hidden":"true"},"#"),n(" AnimatorController")],-1),le=e("p",null,"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.",-1),re=e("p",null,[e("em",null,[n("Create and export "),e("a",{href:"#animatorcontroller"},"animator statemachines"),n(" for controlling complex character animations")])],-1),pe=e("h4",{id:"creating-an-animatorcontroller",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#creating-an-animatorcontroller","aria-hidden":"true"},"#"),n(" Creating an AnimatorController")],-1),ce=e("p",null,"The AnimatorController editor can be opened using the EditorType dropdown in the topleft corner of each panel:",-1),de=e("p",null,[e("img",{src:C,alt:"AnimatorController open window"})],-1),ue=i('
Creating a new animator-controller asset โ or select one from your previously created assets
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),he=e("code",null,".ts",-1),me=e("code",null,"src/scripts",-1),be={href:"http://docs.needle.tools/scripting",target:"_blank",rel:"noopener noreferrer"},ge=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),ke=e("h2",{id:"lightmapping",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#lightmapping","aria-hidden":"true"},"#"),n(" Lightmapping ๐ก")],-1),_e=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),we={class:"custom-container danger"},fe=e("p",{class:"custom-container-title"},"Please keep in mind:",-1),ve={href:"https://discord.needle.tools",target:"_blank",rel:"noopener noreferrer"},ye={href:"https://engine.needle.tools/downloads/blender/lightmaps.blend",target:"_blank",rel:"noopener noreferrer"},xe=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"},Ae=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),Pe=e("h3",{id:"integrated-bugreporter",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#integrated-bugreporter","aria-hidden":"true"},"#"),n(" Integrated Bugreporter")],-1),Se=e("p",null,[e("img",{src:Y,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 Ne(Ee,Ie){const s=r("video-embed"),o=r("router-link"),l=r("ExternalLinkIcon"),c=r("os-link"),d=r("ClientOnly"),p=r("RouterLink");return b(),g("div",null,[M,U,V,W,t(s,{src:"/docs/blender/environment-light.mp4"}),z,F,e("nav",O,[e("ul",null,[e("li",null,[t(o,{to:"#preface"},{default:a(()=>[n("Preface")]),_:1})]),e("li",null,[t(o,{to:"#download-and-installation"},{default:a(()=>[n("Download and Installation ๐ฟ")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#step-1-install-blender-3.6-4.0-4.1-or-4.2"},{default:a(()=>[n("Step 1 โข "),G,n(" 3.6, 4.0, 4.1 or 4.2")]),_:1})]),e("li",null,[t(o,{to:"#step-3-download-needle-engine-for-blender"},{default:a(()=>[n("Step 3 โข "),H]),_:1})])])]),e("li",null,[t(o,{to:"#getting-started"},{default:a(()=>[n("Getting Started ๐ฉ")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#project-panel-overview"},{default:a(()=>[n("Project Panel overview")]),_:1})])])]),e("li",null,[t(o,{to:"#blender-settings"},{default:a(()=>[n("Blender Settings")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#color-management"},{default:a(()=>[n("Color Management")]),_:1})])])]),e("li",null,[t(o,{to:"#environment-lighting"},{default:a(()=>[n("Environment Lighting")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#add-your-custom-hdri-exr-environment-lighting-and-skybox"},{default:a(()=>[n("Add your custom HDRi / EXR environment lighting and skybox")]),_:1})])])]),e("li",null,[t(o,{to:"#export"},{default:a(()=>[n("Export")]),_:1})]),e("li",null,[t(o,{to:"#animation"},{default:a(()=>[n("Animation ๐")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#animatorcontroller"},{default:a(()=>[n("AnimatorController")]),_:1})]),e("li",null,[t(o,{to:"#timeline-nla-tracks-export"},{default:a(()=>[n("Timeline โ nla tracks export ๐ฌ")]),_:1})])])]),e("li",null,[t(o,{to:"#interactivity"},{default:a(()=>[n("Interactivity ๐")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#custom-components"},{default:a(()=>[n("Custom Components")]),_:1})])])]),e("li",null,[t(o,{to:"#lightmapping"},{default:a(()=>[n("Lightmapping ๐ก")]),_:1})]),e("li",null,[t(o,{to:"#texture-compression"},{default:a(()=>[n("Texture Compression")]),_:1})]),e("li",null,[t(o,{to:"#updating"},{default:a(()=>[n("Updating")]),_:1})]),e("li",null,[t(o,{to:"#debugging-reporting-a-problem"},{default:a(()=>[n("Debugging / Reporting a problem")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#integrated-bugreporter"},{default:a(()=>[n("Integrated Bugreporter")]),_:1})])])])])]),X,e("p",null,[J,n(" when it comes to deciding which of those features should be prioritizes."),$,n(" If you have feedback for us please let us know in "),e("a",K,[n("our forum"),t(l)]),n("!")]),Q,t(d,null,{default:a(()=>[n(" ### Step 2 โข "),t(c,{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:a(()=>[n("Install Nodejs โญ")]),_:1})]),_:1}),Z,e("ul",null,[e("li",null,[e("a",ee,[n("Download Blender Samples"),t(l)])])]),ne,t(s,{limit_height:"",max_height:"300px",src:"/docs/blender/environment.mp4"}),te,ae,oe,t(s,{limit_height:"",src:"/docs/blender/custom_hdri.mp4"}),n(),se,t(s,{limit_height:"",src:"/docs/blender/animation.mp4"}),n(),ie,le,t(s,{src:"/docs/blender/animatorcontroller-web.mp4"}),re,pe,ce,de,t(s,{limit_height:"",max_height:"188px",src:"/docs/blender/animatorcontroller-create.mp4"}),n(),ue,e("p",null,[n("To create custom components open the workspace via the Needle Project panel and add a "),he,n(" script file in "),me,n(" inside your web project. Please refer to the "),e("a",be,[n("scripting documentation"),t(l)]),n(" to learn how to write custom components for Needle Engine.")]),ge,ke,_e,e("div",we,[fe,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",ve,[n("our discord"),t(l)]),n(" ๐")])]),t(s,{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",ye,[n("here"),t(l)]),n(".")])]),xe,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 "),je,n(" tab and open the "),Ce,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.")]),Te,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(l)]),n(" for fast support.")]),Ae,Pe,Se])}const Ye=m(q,[["render",Ne],["__file","index.html.vue"]]);export{Ye as default};
diff --git a/assets/index.html-cb603ab8.js b/assets/index.html-cb603ab8.js
new file mode 100644
index 000000000..d8fa976bb
--- /dev/null
+++ b/assets/index.html-cb603ab8.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-f6820c83.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-cfe2bf15.js b/assets/index.html-cfe2bf15.js
new file mode 100644
index 000000000..1a09f1a07
--- /dev/null
+++ b/assets/index.html-cfe2bf15.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-f6820c83.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-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-dccb5a4a.js b/assets/index.html-dccb5a4a.js
new file mode 100644
index 000000000..07c3b1a0a
--- /dev/null
+++ b/assets/index.html-dccb5a4a.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-f6820c83.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-dda688da.js b/assets/index.html-dda688da.js
new file mode 100644
index 000000000..ca6146147
--- /dev/null
+++ b/assets/index.html-dda688da.js
@@ -0,0 +1 @@
+import{_ as u,M as s,p as h,q as m,N as t,V as n,t as o,Q as _,a1 as p,R as e}from"./framework-f6820c83.js";const g="/docs/imgs/banner.webp",f={},w=p('
Needle Engine is a web engine for complex and simple 3D applications alike. Work on your machine and deploy anywhere. Needle Engine 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. These Integrations allow you to use editor features for creating models, authoring materials, animating and sequencing animations, baking lightmaps and more with ease.
Our powerful compression and optimization pipeline for the web make sure your files are ready, small and load fast.
',4),b=e("div",null,[o("Unbelievable Unity editor integration by an order of magnitude,"),e("br"),o("and as straightforward as the docs claim. Wow. โ Chris Mahoney")],-1),v=e("div",null,"This is the best thing I have seen after cinemachine in Unity โ Rinesh Thomas",-1),y=e("div",null,[o("Spent the last 2.5 months building this game, never built a game or used Unity before"),e("br"),o("but absolutely loving the whole process. So rapid! โ Matthew Pieri")],-1),k=e("div",null,[e("a",{href:"https://needle.tools?utm_source=needle_docs&utm_content=quote"},"needle.tools"),o(" is a wonderful showcase of what Needle contributes to 3D via the web. I just love it. โ Kevin Curry")],-1),x=e("div",null,"Played with this a bit this morning ๐คฏ๐คฏ pretty magical โ Brit Gardner",-1),N=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),T=e("div",null,"This is amazing and if you are curious about WebXR with Unity this will help us get there โ Dilmer Valecillos",-1),W=e("div",null,"We just gotta say WOW ๐คฉ โ Unity for Digital Twins",-1),B=e("br",null,null,-1),C=e("br",null,null,-1),U=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),V=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),q=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),D=e("p",null,null,-1);function R(S,j){const a=s("quoteslides"),i=s("action"),l=s("actiongroup"),r=s("copyright"),d=s("removeserviceworker"),c=s("ClientOnly");return h(),m("div",null,[w,t(a,null,{default:n(()=>[b,v,y,k,x,N,T,W]),_:1}),t(l,null,{default:n(()=>[t(i,{href:"getting-started"},{default:n(()=>[o(" Get started โญ ")]),_:1}),t(i,{href:"features-overview"},{default:n(()=>[o(" Features ๐จ ")]),_:1}),t(i,{href:"https://engine.needle.tools/samples?utm_source=needle_docs&utm_content=actionbutton"},{default:n(()=>[o(" Samples ๐ ")]),_:1}),t(i,{href:"https://forum.needle.tools?utm_source=needle_docs&utm_content=actionbutton"},{default:n(()=>[o(" Forum ๐ฌ ")]),_:1})]),_:1}),_(' '),B,C,t(l,null,{default:n(()=>[U,V,q]),_:1}),D,t(r),t(c,null,{default:n(()=>[t(d)]),_:1})])}const I=u(f,[["render",R],["__file","index.html.vue"]]);export{I as default};
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-de7540da.js b/assets/index.html-de7540da.js
new file mode 100644
index 000000000..4abee1f7b
--- /dev/null
+++ b/assets/index.html-de7540da.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-f6820c83.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-de8c7aa7.js b/assets/index.html-de8c7aa7.js
new file mode 100644
index 000000000..152766d6f
--- /dev/null
+++ b/assets/index.html-de8c7aa7.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-f6820c83.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-ee156bf2.js b/assets/index.html-ee156bf2.js
new file mode 100644
index 000000000..947a53bcd
--- /dev/null
+++ b/assets/index.html-ee156bf2.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-f6820c83.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-ee5a34cc.js b/assets/index.html-ee5a34cc.js
new file mode 100644
index 000000000..ce4efbdb1
--- /dev/null
+++ b/assets/index.html-ee5a34cc.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":true,"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.\\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\\n\\nYour feedback is invaluable when it comes to deciding which of those features should be prioritizes.\\nIf you have feedback for us please let us know in our forum!"}]],"description":"---\\nThank you for using Needle Engine for Blender.\\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\\n\\nYour feedback is invaluable when it comes to deciding which of those features should be prioritizes.\\nIf you have feedback for us please let us know in our forum!"},"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, 4.0, 4.1 or 4.2","slug":"step-1-install-blender-3.6-4.0-4.1-or-4.2","link":"#step-1-install-blender-3.6-4.0-4.1-or-4.2","children":[]},{"level":3,"title":"Step 3 โข Download Needle Engine for Blender","slug":"step-3-download-needle-engine-for-blender","link":"#step-3-download-needle-engine-for-blender","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":"Environment Lighting","slug":"environment-lighting","link":"#environment-lighting","children":[{"level":3,"title":"Add your custom HDRi / EXR environment lighting and skybox","slug":"add-your-custom-hdri-exr-environment-lighting-and-skybox","link":"#add-your-custom-hdri-exr-environment-lighting-and-skybox","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":{"updatedTime":1724756390000},"filePathRelative":"blender/index.md"}`);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-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/index.html-fd183cb5.js b/assets/index.html-fd183cb5.js
new file mode 100644
index 000000000..5bfdbdfea
--- /dev/null
+++ b/assets/index.html-fd183cb5.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":"---\\nWith Needle Engine, you can create fully interactive 3D websites.\\nThey can be deployed anywhere on the web and get optimized automatically by the Needle Engine Build Pipeline.\\nNeedle Engine is available as a download for Unity, for Blender, and for web projects without an editor integration."}]],"description":"---\\nWith Needle Engine, you can create fully interactive 3D websites.\\nThey can be deployed anywhere on the web and get optimized automatically by the Needle Engine Build Pipeline.\\nNeedle Engine is available as a download for Unity, for Blender, and for web projects without an editor integration."},"headers":[{"level":2,"title":"Needle Engine for Unity","slug":"needle-engine-for-unity","link":"#needle-engine-for-unity","children":[]},{"level":2,"title":"Needle Engine for Blender","slug":"needle-engine-for-blender","link":"#needle-engine-for-blender","children":[]},{"level":2,"title":"Needle Engine without Editor Integration","slug":"needle-engine-without-editor-integration","link":"#needle-engine-without-editor-integration","children":[]},{"level":2,"title":"Third-Party Dependencies","slug":"third-party-dependencies","link":"#third-party-dependencies","children":[{"level":3,"title":"Recommended Dependencies","slug":"recommended-dependencies","link":"#recommended-dependencies","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":{"updatedTime":1724788496000},"filePathRelative":"getting-started/index.md"}`);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-32ee71cc.js b/assets/meta-test.html-32ee71cc.js
new file mode 100644
index 000000000..617076b5a
--- /dev/null
+++ b/assets/meta-test.html-32ee71cc.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":{"updatedTime":1671370582000},"filePathRelative":"_meta-test.md"}');export{e as data};
diff --git a/assets/meta-test.html-ba3f3381.js b/assets/meta-test.html-ba3f3381.js
new file mode 100644
index 000000000..c9abac796
--- /dev/null
+++ b/assets/meta-test.html-ba3f3381.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-f6820c83.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-e99a74bf.js b/assets/metalink-e99a74bf.js
new file mode 100644
index 000000000..0f11e5be9
--- /dev/null
+++ b/assets/metalink-e99a74bf.js
@@ -0,0 +1 @@
+import{_ as e}from"./framework-f6820c83.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-d4ea4cf8.js b/assets/modules.html-d4ea4cf8.js
new file mode 100644
index 000000000..c96add963
--- /dev/null
+++ b/assets/modules.html-d4ea4cf8.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-f6820c83.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/modules.html-ffce8241.js b/assets/modules.html-ffce8241.js
new file mode 100644
index 000000000..379916890
--- /dev/null
+++ b/assets/modules.html-ffce8241.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-bb6009aa","path":"/modules.html","title":"Additional Modules","lang":"en-US","frontmatter":{"title":"Additional Modules","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/additional modules.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":{"updatedTime":1725399379000},"filePathRelative":"modules.md"}');export{e as data};
diff --git a/assets/needle-button-a6fac229.js b/assets/needle-button-a6fac229.js
new file mode 100644
index 000000000..1985f3439
--- /dev/null
+++ b/assets/needle-button-a6fac229.js
@@ -0,0 +1 @@
+import{_ as l,p as i,q as c,R as r,s as d,w as s,ad as u}from"./framework-f6820c83.js";const n={props:{href:String,secondary:Boolean,same_tab:Boolean,large:Boolean,event_goal:String,event_position:String}},o=()=>{u(t=>({"67fdb382":t.secondary?"#aaa":"#826bed","0b33cc28":t.large?"1em":".8em","1b4cfd03":t.secondary?"#bbb":"#6248be"}))},_=n.setup;n.setup=_?(t,a)=>(o(),_(t,a)):o;const b=["href","target"];function v(t,a,e,f,g,m){return i(),c("a",{href:e.href,target:e.same_tab?"_self":"_blank",class:s(e.event_goal?"plausible-event-name="+e.event_goal+(e.event_position?" plausible-event-position="+e.event_position:""):"")},[r("button",{class:s(e.event_goal?"plausible-event-name="+e.event_goal+(e.event_position?" plausible-event-position="+e.event_position:""):"")},[d(t.$slots,"default",{},void 0,!0)],2)],10,b)}const B=l(n,[["render",v],["__scopeId","data-v-b590c0c3"],["__file","needle-button.vue"]]);export{B as default};
diff --git a/assets/needle-config-json.html-aac10705.js b/assets/needle-config-json.html-aac10705.js
new file mode 100644
index 000000000..c7301ee13
--- /dev/null
+++ b/assets/needle-config-json.html-aac10705.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-f7b8f1f2","path":"/reference/needle-config-json.html","title":"needle.config.json","lang":"en-US","frontmatter":{"title":"needle.config.json","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/needle.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":{"updatedTime":1725399379000},"filePathRelative":"reference/needle-config-json.md"}');export{e as data};
diff --git a/assets/needle-config-json.html-f91034da.js b/assets/needle-config-json.html-f91034da.js
new file mode 100644
index 000000000..62f8b509a
--- /dev/null
+++ b/assets/needle-config-json.html-f91034da.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-f6820c83.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-engine-attributes.html-2b93dca4.js b/assets/needle-engine-attributes.html-2b93dca4.js
new file mode 100644
index 000000000..df57ca292
--- /dev/null
+++ b/assets/needle-engine-attributes.html-2b93dca4.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-caed7310","path":"/reference/needle-engine-attributes.html","title":" Configuration","lang":"en-US","frontmatter":{"title":" Configuration","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/ configuration.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":{"updatedTime":1725399379000},"filePathRelative":"reference/needle-engine-attributes.md"}');export{e as data};
diff --git a/assets/needle-engine-attributes.html-b6293290.js b/assets/needle-engine-attributes.html-b6293290.js
new file mode 100644
index 000000000..62e3e4d6b
--- /dev/null
+++ b/assets/needle-engine-attributes.html-b6293290.js
@@ -0,0 +1,20 @@
+import{_ as l}from"./custom-loading-style-65a23c0d.js";import{_ as o,M as d,p as c,q as u,R as n,t,N as a,a1 as s}from"./framework-f6820c83.js";const i={},r=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),p=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,[t("optional, URL to a skybox image (background image) or a preset string: "),n("code",null,"studio"),t(", "),n("code",null,"blurred-skybox"),t(", "),n("code",null,"quicklook"),t(", "),n("code",null,"quicklook-ar")])],-1),q=n("tr",null,[n("td",null,[n("code",null,"environment-image")]),n("td",null,[t("optional, URL to a environment image (environment light) or a preset string: "),n("code",null,"studio"),t(", "),n("code",null,"blurred-skybox"),t(", "),n("code",null,"quicklook"),t(", "),n("code",null,"quicklook-ar")])],-1),w=n("tr",null,[n("td",null,[n("code",null,"contactshadows")]),n("td",null,"optional, render contact shadows")],-1),R=n("tr",null,[n("td",null,[n("code",null,"tone-mapping")]),n("td",null,[t("optional, supported values are "),n("code",null,"none"),t(", "),n("code",null,"linear"),t(", "),n("code",null,"neutral"),t(", "),n("code",null,"agx")])],-1),D=n("tr",null,[n("td",null,[n("code",null,"tone-mapping-exposure")]),n("td",null,[t("optional number e.g. increase exposure with "),n("code",null,'tone-mapping-exposure="1.5"'),t(", requires "),n("code",null,"tone-mapping"),t(" to be set")])],-1),P=n("tr",null,[n("td",null,[n("strong",null,"Interaction")]),n("td")],-1),C=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),z=n("tr",null,[n("td",null,[n("strong",null,"Internal")]),n("td")],-1),K=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),M=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),X=n("img",{src:l,alt:"custom loading"},null,-1),J=n("br",null,null,-1),Q={href:"https://github.com/needle-engine/vite-template/blob/loading-style/custom/index.html",target:"_blank",rel:"noopener noreferrer"};function W(Z,$){const e=d("ExternalLinkIcon");return c(),u("div",null,[r,n("table",null,[p,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,z,K])]),M,n("p",null,[t("Setting environment images, playing animation and automatic camera controls. "),n("a",Y,[t("See it live on stackblitz"),a(e)])]),G,n("p",null,[X,J,n("a",Q,[t("See code on github"),a(e)])])])}const en=o(i,[["render",W],["__file","needle-engine-attributes.html.vue"]]);export{en as default};
diff --git a/assets/networking.html-91b3a83a.js b/assets/networking.html-91b3a83a.js
new file mode 100644
index 000000000..2eae5ec77
--- /dev/null
+++ b/assets/networking.html-91b3a83a.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":{"updatedTime":1698329319000},"filePathRelative":"networking.md"}');export{e as data};
diff --git a/assets/networking.html-e1e616de.js b/assets/networking.html-e1e616de.js
new file mode 100644
index 000000000..e76f1cf46
--- /dev/null
+++ b/assets/networking.html-e1e616de.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-f6820c83.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/os-link-31cc79d0.js b/assets/os-link-31cc79d0.js
new file mode 100644
index 000000000..3b9c7c69a
--- /dev/null
+++ b/assets/os-link-31cc79d0.js
@@ -0,0 +1 @@
+import{_ as s,p as e,q as n,s as o,t as l,v as u}from"./framework-f6820c83.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-a65539aa.js b/assets/project-structure.html-a65539aa.js
new file mode 100644
index 000000000..d65a1cc1c
--- /dev/null
+++ b/assets/project-structure.html-a65539aa.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":{"updatedTime":1696452754000},"filePathRelative":"project-structure.md"}');export{e as data};
diff --git a/assets/project-structure.html-ff62d7b3.js b/assets/project-structure.html-ff62d7b3.js
new file mode 100644
index 000000000..9f6db6c21
--- /dev/null
+++ b/assets/project-structure.html-ff62d7b3.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-f6820c83.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/quoteslides-a8d399c2.js b/assets/quoteslides-a8d399c2.js
new file mode 100644
index 000000000..0431a1f14
--- /dev/null
+++ b/assets/quoteslides-a8d399c2.js
@@ -0,0 +1 @@
+import{d as n}from"./app-1ecc50a8.js";import{_ as l,o as d,p as a,q as i,s as r}from"./framework-f6820c83.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/removeserviceworker-8e285252.js b/assets/removeserviceworker-8e285252.js
new file mode 100644
index 000000000..ae8ececca
--- /dev/null
+++ b/assets/removeserviceworker-8e285252.js
@@ -0,0 +1 @@
+import{_ as t}from"./framework-f6820c83.js";console.log("ServiceWorker:",navigator.serviceWorker);var o;(o=navigator.serviceWorker)==null||o.getRegistrations().then(r=>{for(const e of r)console.log("ServiceWorker unregistered:",e),e.unregister()});const s={};function n(r,e,i,c,a,v){return null}const f=t(s,[["render",n],["__file","removeserviceworker.vue"]]);export{f as default};
diff --git a/assets/sample-d7ec0e26.js b/assets/sample-d7ec0e26.js
new file mode 100644
index 000000000..9880067ab
--- /dev/null
+++ b/assets/sample-d7ec0e26.js
@@ -0,0 +1 @@
+import{_ as c,p as r,q as s,R as l,Q as i}from"./framework-f6820c83.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}`),typeof window<"u"&&new URL(window.location.href),e.searchParams.append("hideClose",""),e.searchParams.append("utm_source","needle_docs"),e.searchParams.append("utm_content","sample_embed"),this.sanitizedUrl=e.toString()}}},mounted(){const n=e=>{if(!e||!e.contentWindow)return;const t=e.contentWindow.document.getElementsByTagName("iframe")[0];if(t){const a=t.contentWindow.document.querySelector("needle-engine");a&&(a.style.touchAction="pan-y")}};n(this.$refs.frame1),n(this.$refs.frame2)}},m=["src"],p=["src"];function u(n,e,t,o,a,_){return r(),s("div",null,[l("iframe",{src:a.sanitizedUrl,ref:"frame1",allow:"xr; xr-spatial-tracking; camera; microphone; fullscreen;display-capture"},null,8,m),t.split===!0?(r(),s("iframe",{key:0,src:a.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",u],["__scopeId","data-v-65d547d3"],["__file","sample.vue"]]);export{h as default};
diff --git a/assets/samples-and-modules.html-57e9f6ca.js b/assets/samples-and-modules.html-57e9f6ca.js
new file mode 100644
index 000000000..12b9c047a
--- /dev/null
+++ b/assets/samples-and-modules.html-57e9f6ca.js
@@ -0,0 +1,31 @@
+import{_ as i,M as s,p as r,q as p,N as t,R as n,t as e,Q as o}from"./framework-f6820c83.js";const d={},m=n("h2",{id:"samples-to-download-and-play",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#samples-to-download-and-play","aria-hidden":"true"},"#"),e(" Samples to download and play")],-1),c={href:"https://engine.needle.tools/samples",target:"_blank",rel:"noopener noreferrer"};function h(u,g){const a=s("sample"),l=s("ExternalLinkIcon");return r(),p("div",null,[t(a,{src:"https://engine.needle.tools/samples-uploads/physics-cannon/"}),m,n("p",null,[e("View all samples at "),n("a",c,[e("engine.needle.tools/samples"),t(l)]),e(" with a live preview and links for download and installation.")]),o(` - [Brune Simons 20k](https://engine.needle.tools/samples/buno-simon-20k)
+ *Physics*
+- [Stencil Portals](https://engine.needle.tools/samples/stencil-portals)
+ *URP RenderObject stencil to threejs*
+- [Custom Shaders](https://engine.needle.tools/samples/custom-shaders)
+ *Unity ShaderGraph to threejs*
+- [Sandbox](https://needle-tiny-starter.glitch.me/)
+ *Multi-user sandbox builder*
+- [VideoPlayer](https://needle-videoplayer-sample.glitch.me/)
+ *Minimal Unity VideoPlayer sample*
+- [UI Button](https://engine.needle.tools/samples/ui-button)
+ *Minimal UI button event sample*
+- [Spatial Trigger](https://needle-spatial-trigger-sample.glitch.me/)
+ *Define areas in your world and hookup events from Unity*
+- and [more](https://engine.needle.tools/samples/)
+
+---
+
+
+
+
+
+
+
+
+
+
+
+ `),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/samples-and-modules.html-a3fd2522.js b/assets/samples-and-modules.html-a3fd2522.js
new file mode 100644
index 000000000..8dfe4c3a2
--- /dev/null
+++ b/assets/samples-and-modules.html-a3fd2522.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
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),E=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),U={href:"https://stackblitz.com/edit/needle-engine-cycle-src?file=index.html",target:"_blank",rel:"noopener noreferrer"},N=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),W={href:"https://github.com/pmndrs/postprocessing",target:"_blank",rel:"noopener noreferrer"},Y=n("code",null,"npm i postprocessing",-1),K=n("code",null,"PostProcessingEffect",-1),Q=n("p",null,[s("To use the effect add it to the same object as your "),n("code",null,"Volume"),s(" component.")],-1),J={href:"https://pmndrs.github.io/postprocessing/public/demo/#outline",target:"_blank",rel:"noopener noreferrer"},X=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 sn(an,tn){const t=p("ExternalLinkIcon"),o=p("stackblitz"),c=p("video-embed");return l(),u("div",null,[d,n("p",null,[n("a",v,[s("Video tutorial: How to write custom components"),a(t)])]),m,b,n("ul",null,[n("li",null,[n("a",h,[s("Visit Samples Website"),a(t)])]),n("li",null,[n("a",y,[s("Download Samples Package"),a(t)])]),n("li",null,[n("a",w,[s("Needle Engine Stackblitz Collection"),a(t)])]),n("li",null,[n("a",g,[s("Needle Engine API"),a(t)])])]),f,a(o,{file:"@code/basic-component.ts"}),s(),x,n("div",_,[q,n("p",null,[s("Find a "),n("a",C,[s("working example in our samples"),a(t)]),s(" to download and try")])]),S,a(o,{file:"@code/component-click.ts"},{default:r(()=>[s(" test ")]),_:1}),E,n("div",j,[B,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 "),O,s(" as well."),M,s(" This is an experimental feature: please provide feedback in our "),n("a",L,[s("discord"),a(t)])])]),R,n("div",z,[P,n("p",null,[s("Keep in mind that you still have access to all web apis and "),n("a",T,[s("npm"),a(t)]),s(" packages!"),D,s(" That's the beauty of Needle Engine if we're allowed to say this here ๐")])]),A,a(c,{src:"./videos/component-time.mp4",limit_height:""}),H,n("p",null,[s("Assuming you have a custom shader with a property name "),V,s(" that is a float value this is how you would change it from a script."),G,s(" You can find a live "),n("a",F,[s("example to download in our samples"),a(t)])]),I,n("p",null,[s("See "),n("a",U,[s("live example"),a(t)]),s(" on StackBlitz")]),N,n("p",null,[s("Make sure to install "),n("a",W,[Y,a(t)]),s(" in your web project. Then you can add new effects by deriving from "),K,s(".")]),Q,n("p",null,[s("Here is an example that wraps the "),n("a",J,[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.")]),X,n("p",null,[s("Make sure to install the mediapipe package. Visit the github link below to see the complete project setup."),$,s(" Try it "),n("a",Z,[s("live here"),a(t)]),s(" - requires a webcam/camera")]),nn])}const pn=i(k,[["render",sn],["__file","scripting-examples.html.vue"]]);export{pn as default};
diff --git a/assets/scripting.html-0734bdfb.js b/assets/scripting.html-0734bdfb.js
new file mode 100644
index 000000000..b47278ca2
--- /dev/null
+++ b/assets/scripting.html-0734bdfb.js
@@ -0,0 +1,157 @@
+import{_ as p,M as i,p as l,q as r,R as e,N as s,V as c,t as n,a1 as a}from"./framework-f6820c83.js";const d={},u=e("p",null,"If you are new to scripting we recommend reading the following guides first:",-1),h={href:"https://engine.needle.tools/docs/api/latest",target:"_blank",rel:"noopener noreferrer"},m=e("hr",null,null,-1),k={href:"https://typescriptlang.org",target:"_blank",rel:"noopener noreferrer"},b={href:"https://javascript.info/",target:"_blank",rel:"noopener noreferrer"},v=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),g={href:"https://stackblitz.com/fork/github/needle-engine/vite-template?file=src%2Fmain.ts",target:"_blank",rel:"noopener noreferrer"},y=a('
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),f=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),w=e("br",null,null,-1),x=e("br",null,null,-1),C=e("code",null,"Create > NPM Definition",-1),_=e("code",null,"Create > TypeScript",-1),j=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),R=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),E=a(`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 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),O={class:"custom-container details"},D=e("summary",null,"Create component with a custom function",-1),T=a(`
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(cb, options)
Called when a new context is initialized (before the first frame)
onClear(cb, options)
Register a callback before the engine context is cleared
onDestroy(cb, options)
Register a callback in the engine before the context is destroyed
onStart(cb, options)
Called directly after components start at the beginning of a frame
// this can be put into e.g. main.ts or a svelte component (similar to onMount)
+import{ onUpdate, onBeforeRender }from"@needle-tools/engine"
+onUpdate((ctx: Context)=>{
+ // do something... e.g. access the scene via ctx.scene
+ console.log("UPDATE", ctx.time.frame);
+});
+
+onBeforeRender((ctx: Context)=>{
+ // this event is only called once because of the { once: true } argument
+ console.log("ON BEFORE RENDER", ctx.time.frame);
+},{ once:true});
+
+// Every event hook returns a method to unsubscribe from the event
+const unsubscribe =onAfterRender((ctx: Context)=>{
+ console.log("ON AFTER RENDER", ctx.time.frame);
+});
+// Unsubscribe from the event at any time
+setTimeout(()=>unsubscribe(),1000);
+
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 rn(dn,un){const o=i("RouterLink"),t=i("ExternalLinkIcon");return l(),r("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})])]),e("p",null,[n("If you know what you're doing, feel free to jump right into the "),e("a",h,[n("Needle Engine API documentation"),s(t)]),n(".")]),m,e("p",null,[n("Runtime code for Needle Engine is written in "),e("a",k,[n("TypeScript"),s(t)]),n(" (recommended) or "),e("a",b,[n("JavaScript"),s(t)]),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(".")]),v,e("ul",null,[e("li",null,[e("a",g,[n("Create virtual workspace to code along"),s(t)]),n(".")])]),y,e("ul",null,[f,e("li",null,[e("p",null,[n("Unity specific:"),w,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."),x,n(" In Unity you can create NpmDef files via "),C,n(" and then add TypeScript files by right-clicking an NpmDef file and selecting "),_,n(". Please see "),s(o,{to:"/project-structure.html#npm-definition-files"},{default:c(()=>[n("this chapter")]),_:1}),n(" for more information.")])])]),j,R,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.")]),E,e("details",O,[D,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.")]),T]),q,e("p",null,[n("Coroutines can be declared using the "),e("a",N,[n("JavaScript Generator Syntax"),s(t)]),n("."),S,n(" To start a coroutine, call "),I]),z,e("p",null,[n("For example ("),e("a",U,[n("See example on stackblitz"),s(t)]),n(")")]),P,e("p",null,[n("The context refers to the runtime inside a "),e("a",A,[n("web component"),s(t)]),n("."),G,n(" The three.js scene lives inside a custom HTML component called "),X,n(" (see the "),B,n(" in your project). You can access the "),M,n(" web component using "),F,n(".")]),L,e("p",null,[n("You can also use three.js specific methods to quickly iterate all objects recursively using the "),e("a",Y,[W,s(t)]),n(" method:")]),V,e("p",null,[n("or to just traverse visible objects use "),e("a",J,[H,s(t)]),n(" instead.")]),K,e("p",null,[n("If you want to handle inputs yourself you can also subscribe to "),e("a",Q,[n("all events the browser provides"),s(t)]),n(" (there are a ton). For example to subscribe to the browsers click event you can write:")]),Z,e("p",null,[n("Use "),$,n(" to perform a raycast using a "),e("a",nn,[n("three.js ray"),s(t)])]),en,e("p",null,[n("Here is a editable "),e("a",sn,[n("example for physics raycast"),s(t)])]),tn,e("p",null,[n("Networking methods can be accessed via "),an,n(". Please refer to the "),s(o,{to:"/networking.html"},{default:c(()=>[n("networking docs")]),_:1}),n(" for further information.")]),on,e("p",null,[n("Referenced Prefabs, SceneAssets and "),e("a",cn,[pn,s(t)]),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).")]),ln])}const mn=p(d,[["render",rn],["__file","scripting.html.vue"]]);export{mn as default};
diff --git a/assets/scripting.html-2ce40752.js b/assets/scripting.html-2ce40752.js
new file mode 100644
index 000000000..034e3ee1b
--- /dev/null
+++ b/assets/scripting.html-2ce40752.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-fab6318a","path":"/scripting.html","title":"Creating and using Components","lang":"en-US","frontmatter":{"title":"Creating and using Components","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/creating and using components.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":{"updatedTime":1725399379000},"filePathRelative":"scripting.md"}`);export{e as data};
diff --git a/assets/showcase-bike.html-aed7c46a.js b/assets/showcase-bike.html-aed7c46a.js
new file mode 100644
index 000000000..13508d26c
--- /dev/null
+++ b/assets/showcase-bike.html-aed7c46a.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-f6820c83.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-bike.html-baf689ea.js b/assets/showcase-bike.html-baf689ea.js
new file mode 100644
index 000000000..91cc65171
--- /dev/null
+++ b/assets/showcase-bike.html-baf689ea.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":{"updatedTime":1669763662000},"filePathRelative":"showcase-bike.md"}');export{e as data};
diff --git a/assets/showcase-castle.html-86fe547b.js b/assets/showcase-castle.html-86fe547b.js
new file mode 100644
index 000000000..34d7f4d50
--- /dev/null
+++ b/assets/showcase-castle.html-86fe547b.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":{"updatedTime":1669763662000},"filePathRelative":"showcase-castle.md"}`);export{e as data};
diff --git a/assets/showcase-castle.html-cdb5db57.js b/assets/showcase-castle.html-cdb5db57.js
new file mode 100644
index 000000000..da6ed1c60
--- /dev/null
+++ b/assets/showcase-castle.html-cdb5db57.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-f6820c83.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-555f99b8.js b/assets/showcase-mercedes-benz.html-555f99b8.js
new file mode 100644
index 000000000..abbc562a3
--- /dev/null
+++ b/assets/showcase-mercedes-benz.html-555f99b8.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":{"updatedTime":1668622149000},"filePathRelative":"showcase-mercedes-benz.md"}`);export{e as data};
diff --git a/assets/showcase-mercedes-benz.html-7eabd1f0.js b/assets/showcase-mercedes-benz.html-7eabd1f0.js
new file mode 100644
index 000000000..8bb5c0faf
--- /dev/null
+++ b/assets/showcase-mercedes-benz.html-7eabd1f0.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-f6820c83.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.
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 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 tools like Unity or Blender into spatial web development powerhouses โ adding glTF assets to the typical HTML, CSS, JavaScript 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-f395cd1d.js b/assets/technical-overview.html-f395cd1d.js
new file mode 100644
index 000000000..493a10ce6
--- /dev/null
+++ b/assets/technical-overview.html-f395cd1d.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":{"updatedTime":1725399379000},"filePathRelative":"technical-overview.md"}`);export{e as data};
diff --git a/assets/testimonial-9717e223.js b/assets/testimonial-9717e223.js
new file mode 100644
index 000000000..d1a5289b2
--- /dev/null
+++ b/assets/testimonial-9717e223.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-f6820c83.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-e5440bb3.js b/assets/testimonials.html-e5440bb3.js
new file mode 100644
index 000000000..a5af0a08f
--- /dev/null
+++ b/assets/testimonials.html-e5440bb3.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":{"updatedTime":1725399379000},"filePathRelative":"testimonials.md"}');export{e as data};
diff --git a/assets/testimonials.html-ee19aec9.js b/assets/testimonials.html-ee19aec9.js
new file mode 100644
index 000000000..78b54b276
--- /dev/null
+++ b/assets/testimonials.html-ee19aec9.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-f6820c83.js";const h={},c=i("h1",{id:"testimonials",tabindex:"-1"},[i("a",{class:"header-anchor",href:"#testimonials","aria-hidden":"true"},"#"),e(" Testimonials")],-1),d=i("p",null,null,-1);function m(u,p){const t=o("testimonial");return r(),l("div",null,[c,d,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}),s(t,{name:"Yuzu",src:"https://discord.com/channels/717429793926283276/1264966222467043433/1265268833485066240"},{default:a(()=>[e(" My workflow has been optimized 10X ever since i started using needle ")]),_:1})])}const f=n(h,[["render",m],["__file","testimonials.html.vue"]]);export{f as default};
diff --git a/assets/testing.html-09ed932c.js b/assets/testing.html-09ed932c.js
new file mode 100644
index 000000000..00dc88859
--- /dev/null
+++ b/assets/testing.html-09ed932c.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-f6820c83.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-4f37a182.js b/assets/testing.html-4f37a182.js
new file mode 100644
index 000000000..70427c886
--- /dev/null
+++ b/assets/testing.html-4f37a182.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":{"updatedTime":1689176384000},"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-64c32bd4.js b/assets/typescript-decorators.html-64c32bd4.js
new file mode 100644
index 000000000..c7a675d3d
--- /dev/null
+++ b/assets/typescript-decorators.html-64c32bd4.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-f6820c83.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-decorators.html-d58d6f08.js b/assets/typescript-decorators.html-d58d6f08.js
new file mode 100644
index 000000000..f6a0c83c2
--- /dev/null
+++ b/assets/typescript-decorators.html-d58d6f08.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-6d28ca94","path":"/reference/typescript-decorators.html","title":"@serializable and other decorators","lang":"en-US","frontmatter":{"title":"@serializable and other decorators","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/@serializable and other 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":{"updatedTime":1725399379000},"filePathRelative":"reference/typescript-decorators.md"}');export{e as data};
diff --git a/assets/typescript-essentials.html-534c1795.js b/assets/typescript-essentials.html-534c1795.js
new file mode 100644
index 000000000..05bcd7e1d
--- /dev/null
+++ b/assets/typescript-essentials.html-534c1795.js
@@ -0,0 +1,83 @@
+import{_ as c,M as o,p as i,q as l,R as n,t as s,N as a,V as r,a1 as t}from"./framework-f6820c83.js";const u={},d=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),k=n("p",null,"Here are also some useful resources for learning how to write Typescript:",-1),m={href:"https://www.typescripttutorial.net/",target:"_blank",rel:"noopener noreferrer"},h={href:"https://www.tutorialsteacher.com/typescript",target:"_blank",rel:"noopener noreferrer"},v={href:"https://www.typescriptlang.org/docs/",target:"_blank",rel:"noopener noreferrer"},y=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),f=n("em",null,"Be aware of var",-1),_=n("br",null,null,-1),x=n("code",null,"var",-1),E=n("code",null,"let",-1),V={href:"https://stackoverflow.com/a/11444416",target:"_blank",rel:"noopener noreferrer"},T=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);
+}
+
`,4);function S(j,U){const e=o("ExternalLinkIcon"),p=o("RouterLink");return i(),l("div",null,[d,k,n("ul",null,[n("li",null,[n("a",m,[s("Typescript Tutorial"),a(e)])]),n("li",null,[n("a",h,[s("Learn Typescript"),a(e)])]),n("li",null,[n("a",v,[s("Typescript Documentation"),a(e)])])]),y,n("p",null,[s("While "),b,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",g,[s("JSDoc"),a(e)])]),s(".")]),w,n("blockquote",null,[n("p",null,[f,_,s(" You might come across the "),x,s(" keyword in javascript as well but it is not recommended to use it and the modern replacement for it is "),E,s(". Learn more about "),n("a",V,[s("var vs let"),a(e)]),s(".")])]),T,n("p",null,[s("You notice that the second variable "),M,s(" is using "),C,s(" which does a loose equality check in which case "),I,s(" and "),O,s(" will both result in "),q,s("here. You can read more about that "),n("a",F,[s("here"),a(e)])]),L,n("p",null,[s("The short and "),J,s(" syntax for doing this is to use "),n("a",N,[s("Arrow Functions"),a(e)]),s(".")]),P,n("ul",null,[n("li",null,[a(p,{to:"/scripting.html"},{default:r(()=>[s("Needle Engine Scripting")]),_:1})])])])}const D=c(u,[["render",S],["__file","typescript-essentials.html.vue"]]);export{D as default};
diff --git a/assets/typescript-essentials.html-67bd72a7.js b/assets/typescript-essentials.html-67bd72a7.js
new file mode 100644
index 000000000..dee7fe3ac
--- /dev/null
+++ b/assets/typescript-essentials.html-67bd72a7.js
@@ -0,0 +1 @@
+const e=JSON.parse(`{"key":"v-2e0e70fc","path":"/getting-started/typescript-essentials.html","title":"Scripting in Needle Engine","lang":"en-US","frontmatter":{"title":"Scripting in Needle Engine","description":"Differences, similarities and key concepts of Typescript, Javascript and C#.","sidebarDepth":2,"head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/scripting in needle engine.png"}],["meta",{"name":"og:description","content":"Differences, similarities and key concepts of Typescript, Javascript and C#."}]]},"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":[]},{"level":2,"title":"What's next?","slug":"what-s-next","link":"#what-s-next","children":[]}],"git":{"updatedTime":1725399379000},"filePathRelative":"getting-started/typescript-essentials.md"}`);export{e as data};
diff --git a/assets/vanilla-js.html-7228f453.js b/assets/vanilla-js.html-7228f453.js
new file mode 100644
index 000000000..32bbbfac3
--- /dev/null
+++ b/assets/vanilla-js.html-7228f453.js
@@ -0,0 +1 @@
+const e=JSON.parse('{"key":"v-7acec8c5","path":"/vanilla-js.html","title":"Using Needle Engine directly from HTML","lang":"en-US","frontmatter":{"title":"Using Needle Engine directly from HTML","head":[["meta",{"name":"og:image","content":"https://engine.needle.tools/docs/.preview/using needle engine directly from html.png"}],["meta",{"name":"og:description","content":"---\\nWhile our default template uses vite as powerful bundler, Needle Engine can also be used directly with vanilla Javascript โ without using any bundler.\\nA basic sample can be found here.\\nMany examples can be found on StackBlitz."}]],"description":"---\\nWhile our default template uses vite as powerful bundler, Needle Engine can also be used directly with vanilla Javascript โ without using any bundler.\\nA basic sample can be found here.\\nMany examples can be found on StackBlitz."},"headers":[{"level":3,"title":"How to run locally","slug":"how-to-run-locally","link":"#how-to-run-locally","children":[]}],"git":{"updatedTime":1725399379000},"filePathRelative":"vanilla-js.md"}');export{e as data};
diff --git a/assets/vanilla-js.html-f7e2599d.js b/assets/vanilla-js.html-f7e2599d.js
new file mode 100644
index 000000000..4d77d2b1d
--- /dev/null
+++ b/assets/vanilla-js.html-f7e2599d.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-f6820c83.js";const i={},u=n("h1",{id:"using-needle-engine-directly-from-html",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#using-needle-engine-directly-from-html","aria-hidden":"true"},"#"),s(" Using Needle Engine directly from HTML")],-1),r={href:"https://vitejs.dev",target:"_blank",rel:"noopener noreferrer"},k={href:"https://engine.needle.tools/samples/vanillajs/",target:"_blank",rel:"noopener noreferrer"},d=n("br",null,null,-1),v={href:"https://stackblitz.com/@marwie/collections/needle-engine",target:"_blank",rel:"noopener noreferrer"},g=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),m={href:"https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer",target:"_blank",rel:"noopener noreferrer"},h=n("li",null,[s("Then open this index.html with the live-server and navigate to "),n("code",null,"http://localhost:5500/index.html"),s(" in your web browser.")],-1),b={class:"custom-container tip"},q=n("p",{class:"custom-container-title"},"TIP",-1),_=n("code",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("While our default template uses "),n("a",r,[s("vite"),t(a)]),s(" as powerful bundler, Needle Engine can also be used directly with vanilla Javascript โ without using any bundler.")]),n("p",null,[s("A basic sample can be "),n("a",k,[s("found here"),t(a)]),s("."),d,s(" Many examples can be found on "),n("a",v,[s("StackBlitz"),t(a)]),s(".")]),g,n("ul",null,[n("li",null,[s("Install the "),n("a",m,[s("VSCode LiveServer extension"),t(a)]),s(".")]),h]),n("div",b,[q,n("p",null,[s("Make sure to update the "),_,s(" path to an existing glb file 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 T=e(i,[["render",j],["__file","vanilla-js.html.vue"]]);export{T as default};
diff --git a/assets/video-embed-0d320302.js b/assets/video-embed-0d320302.js
new file mode 100644
index 000000000..8135557ce
--- /dev/null
+++ b/assets/video-embed-0d320302.js
@@ -0,0 +1 @@
+import{_ as c,p as s,q as i,R as a,Q as l,ad as _}from"./framework-f6820c83.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-9d98bfd6.js b/assets/vision.html-9d98bfd6.js
new file mode 100644
index 000000000..4b4c5519e
--- /dev/null
+++ b/assets/vision.html-9d98bfd6.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":{"updatedTime":1667075212000},"filePathRelative":"vision.md"}`);export{e as data};
diff --git a/assets/vision.html-c80404e1.js b/assets/vision.html-c80404e1.js
new file mode 100644
index 000000000..848dc1c3d
--- /dev/null
+++ b/assets/vision.html-c80404e1.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-f6820c83.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-4b92f962.js b/assets/xr.html-4b92f962.js
new file mode 100644
index 000000000..87204afbd
--- /dev/null
+++ b/assets/xr.html-4b92f962.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":"Examples","slug":"examples","link":"#examples","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":{"updatedTime":1725182787000},"filePathRelative":"xr.md"}`);export{e as data};
diff --git a/assets/xr.html-704d5e10.js b/assets/xr.html-704d5e10.js
new file mode 100644
index 000000000..37ee3a9ec
--- /dev/null
+++ b/assets/xr.html-704d5e10.js
@@ -0,0 +1,16 @@
+import{_ as c,M as i,p as d,q as p,R as e,t,N as n,V as o,a1 as r}from"./framework-f6820c83.js";const u={},h=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
Apple Vision Pro
โ๏ธ Safari Browser
hand tracking, support for transient pointer
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 3
โ๏ธ Meta Browser
hand tracking, support for sessiongranted1, passthrough, depth sensing, mesh tracking
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)
Looking Glass Portrait
โ๏ธ Chrome
requires shim, see samples
',3),m=e("thead",null,[e("tr",null,[e("th",null,"Tested AR Device"),e("th",null,"Browser"),e("th",null,"Notes")])],-1),g=e("tr",null,[e("td",null,"Android 10+"),e("td",null,"โ๏ธ Chrome"),e("td")],-1),b=e("tr",null,[e("td",null,"Android 10+"),e("td",null,"โ๏ธ Firefox"),e("td")],-1),_=e("tr",null,[e("td",null,"iOS 15+"),e("td",null,"โ๏ธ WebXR Viewer"),e("td",null,"does not fully implement standards, but supported")],-1),f=e("td",null,"iOS 15+",-1),k=e("td",null,[t("(โ๏ธ)"),e("sup",null,"3"),t(" Safari")],-1),v=e("tr",null,[e("td",null,"Hololens 2"),e("td",null,"โ๏ธ Edge"),e("td")],-1),y=e("tr",null,[e("td",null,"Hololens 1"),e("td",null,"โ"),e("td",null,"no WebXR support")],-1),w=e("tr",null,[e("td",null,"Magic Leap 2"),e("td",null,"โ๏ธ"),e("td")],-1),R=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),A=e("sup",null,"1",-1),x=e("code",null,"chrome://flags/#webxr-navigation-permission",-1),S=e("br",null,null,-1),T=e("sup",null,"2",-1),E=e("br",null,null,-1),j=e("sup",null,"3",-1),q=e("a",{href:"#augmented-reality-and-webxr-on-ios"},"other approaches",-1),O=e("h2",{id:"examples",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#examples","aria-hidden":"true"},"#"),t(" Examples")],-1),B={href:"https://engine.needle.tools/samples/?overlay=samples&tag=xr",target:"_blank",rel:"noopener noreferrer"},C=r('
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.
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),N={href:"https://github.com/immersive-web/navigation",target:"_blank",rel:"noopener noreferrer"},M=e("code",null,"sessiongranted",-1),P=e("p",null,[t("Currently, this is only supported on Oculus Quest 1, 2 and 3 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"),t(" Requires enabling a browser flag: "),e("code",null,"chrome://flags/#webxr-navigation-permission")],-1),I=e("ul",null,[e("li",null,[e("strong",null,"Click on objects to open links"),e("br"),t(" Add the "),e("code",null,"OpenURL"),t(" component that makes it very easy to build connected worlds.")])],-1),D=e("h2",{id:"scripting",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#scripting","aria-hidden":"true"},"#"),t(" Scripting")],-1),L=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),Q={href:"https://www.w3.org/TR/webxr-dom-overlays-1/#ua-style-sheet-defaults",target:"_blank",rel:"noopener noreferrer"},z=e("em",null,"inside",-1),K=e("code",null,'class="ar"',-1),Y=e("h2",{id:"image-tracking",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#image-tracking","aria-hidden":"true"},"#"),t(" Image Tracking")],-1),Z={href:"https://github.com/immersive-web/marker-tracking/blob/main/explainer.md",target:"_blank",rel:"noopener noreferrer"},J=e("br",null,null,-1),$=e("ul",null,[e("li",null,[t("Enable "),e("code",null,"WebXR Incubations"),t(" in chrome")]),e("li",null,[t("Add the "),e("code",null,"WebXRImageTracking"),t(" component")])],-1),ee=e("p",null,[t("Without that spec, one can still request camera image access and run custom algorithms to determine device pose."),e("br"),t(" Libraries to add image tracking:")],-1),te={href:"https://github.com/AR-js-org/AR.js",target:"_blank",rel:"noopener noreferrer"},ne={href:"https://github.com/FireDragonGameStudio/NeedleAndARjs",target:"_blank",rel:"noopener noreferrer"},ae={href:"https://github.com/hiukim/mind-ar-js",target:"_blank",rel:"noopener noreferrer"},se=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"},"#"),t(" Augmented Reality and WebXR on iOS")],-1),oe=e("p",null,"Augmented Reality experiences on iOS are somewhat limited, due to Apple currently not supporting WebXR on iOS devices.",-1),re=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"},"#"),t(" Musical Instrument โ WebXR and QuickLook support")],-1),ie=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),le=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"},"#"),t(" Everywhere Actions and other options for iOS AR")],-1),ce=e("p",null,"There's also other options for guiding iOS users to even more capable interactive AR experiences:",-1),de={start:"3"},pe=e("strong",null,"Exporting content on-the-fly as USDZ files.",-1),ue=e("br",null,null,-1),he={href:"https://castle.needle.tools",target:"_blank",rel:"noopener noreferrer"},me={href:"https://accurate-tree-observation.glitch.me/",target:"_blank",rel:"noopener noreferrer"},ge=e("br",null,null,-1),be=e("strong",null,"Guiding users towards WebXR-compatible browsers on iOS.",-1),_e={href:"https://apps.apple.com/de/app/webxr-viewer/id1295998056",target:"_blank",rel:"noopener noreferrer"},fe=e("p",null,[e("strong",null,"Using camera access and custom algorithms on iOS devices."),e("br"),t(" One can request camera image access and run custom algorithms to determine device pose."),e("br"),t(" 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),ke={href:"https://github.com/AR-js-org/AR.js",target:"_blank",rel:"noopener noreferrer"},ve={href:"https://github.com/FireDragonGameStudio/NeedleAndARjs",target:"_blank",rel:"noopener noreferrer"},ye={href:"https://github.com/hiukim/mind-ar-js",target:"_blank",rel:"noopener noreferrer"},we={href:"https://www.8thwall.com/",target:"_blank",rel:"noopener noreferrer"},Re=e("h2",{id:"references",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#references","aria-hidden":"true"},"#"),t(" References")],-1),Ae={href:"https://www.w3.org/TR/webxr/",target:"_blank",rel:"noopener noreferrer"},xe=e("br",null,null,-1),Se={href:"https://caniuse.com/webxr",target:"_blank",rel:"noopener noreferrer"},Te=e("br",null,null,-1),Ee={href:"https://developer.apple.com/augmented-reality/quick-look/",target:"_blank",rel:"noopener noreferrer"};function je(qe,Oe){const s=i("RouterLink"),a=i("ExternalLinkIcon"),l=i("sample");return d(),p("div",null,[h,e("table",null,[m,e("tbody",null,[g,b,_,e("tr",null,[f,k,e("td",null,[t("No full code support, but Needle "),n(s,{to:"/everywhere-actions.html"},{default:o(()=>[t("Everywhere Actions")]),_:1}),t(" are supported for creating dynamic, interactive USDZ files.")])]),v,y,w])]),R,e("p",null,[A,t(": Requires enabling a browser flag: "),x,S,T,t(": Requires enabling a toggle in the Developer settings"),E,j,t(": Uses "),n(s,{to:"/everywhere-actions.html"},{default:o(()=>[t("Everywhere Actions")]),_:1}),t(" or "),q]),O,e("p",null,[t("Visit our "),e("a",B,[t("Needle Engine XR Samples"),n(a)]),t(" to try many interactive examples right now!")]),C,e("blockquote",null,[e("p",null,[e("strong",null,[e("a",W,[t("Castle Builder"),n(a)])]),t(" uses all of the above for a cross-platform multiplayer sandbox experience."),X,t(" โ #madebyneedle ๐")])]),V,e("p",null,[t("Needle Engine supports the "),e("a",N,[M,n(a)]),t(" state. This allows users to seamlessly traverse between WebXR applications without leaving an immersive session โ they stay in VR or AR.")]),P,I,D,e("p",null,[t("Read more about scripting for XR at the "),n(s,{to:"/scripting.html#xr-event-methods"},{default:o(()=>[t("scripting XR documentation")]),_:1})]),L,e("p",null,[t("Use the "),G,t(" class to show/hide specific content while in AR. The "),e("a",H,[F,t(" pseudo class"),n(a)]),t(" shouldn't be used at this point because using it breaks Mozilla's WebXR Viewer.")]),U,e("p",null,[t("It's worth noting that the overlay element "),e("a",Q,[t("will be always displayed fullscreen while in XR"),n(a)]),t(", independent of styling that has been applied. If you want to align items differently, you should make a container "),z,t(" the "),K,t(" element.")]),n(l,{src:"https://engine.needle.tools/samples-uploads/ar-overlay/"}),Y,e("p",null,[t('WebXR ImageTracking is still in "draft" phase: '),e("a",Z,[t("Marker Tracking Explainer"),n(a)]),J,t(" But you can still use WebXR ImageTracking with Needle Engine today:")]),$,e("p",null,[t("You can find additional documentation in the "),n(s,{to:"/everywhere-actions.html#image-tracking"},{default:o(()=>[t("Everywhere Actions")]),_:1}),t(" section")]),ee,e("ul",null,[e("li",null,[e("a",te,[t("AR.js"),n(a)]),t(" (open source) "),e("ul",null,[e("li",null,[e("a",ne,[t("Experimental AR.js integration"),n(a)]),t(" by FireDragonGameStudio")])])]),e("li",null,[e("a",ae,[t("Mind AR"),n(a)]),t(" (open source)")])]),se,oe,e("p",null,[t("Needle Engine's "),n(s,{to:"/everywhere-actions.html"},{default:o(()=>[t("Everywhere Actions")]),_:1}),t(" 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 "),n(s,{to:"/everywhere-actions.html"},{default:o(()=>[t("the docs")]),_:1}),t(" for more information.")]),re,ie,n(l,{src:"https://engine.needle.tools/samples-uploads/musical-instrument"}),le,ce,e("ol",de,[e("li",null,[pe,ue,t(" 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",he,[t("Castle Builder"),n(a)]),t(" where creations (not the live session) can be viewed in AR.")])]),e("blockquote",null,[e("p",null,[e("strong",null,[e("a",me,[t("Encryption in Space"),n(a)])]),t(" 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."),ge,t(" โ #madewithneedle by Katja Rempel ๐")])]),e("ol",null,[e("li",null,[e("p",null,[be,t(" Depending on your target audience, you can guide users on iOS towards for example Mozilla's "),e("a",_e,[t("WebXR Viewer"),n(a)]),t(" to experience AR on iOS.")])]),e("li",null,[fe,e("ul",null,[e("li",null,[e("a",ke,[t("AR.js"),n(a)]),t(" (open source) "),e("ul",null,[e("li",null,[e("a",ve,[t("Experimental AR.js integration"),n(a)]),t(" by FireDragonGameStudio")])])]),e("li",null,[e("a",ye,[t("Mind AR"),n(a)]),t(" (open source)")]),e("li",null,[e("a",we,[t("8th Wall"),n(a)]),t(" (commercial)")])])])]),Re,e("p",null,[e("a",Ae,[t("WebXR Device API"),n(a)]),xe,e("a",Se,[t("caniuse: WebXR"),n(a)]),Te,e("a",Ee,[t("Apple's Preliminary USD Behaviours"),n(a)])])])}const Ce=c(u,[["render",je],["__file","xr.html.vue"]]);export{Ce as default};
diff --git a/backlog-mermaid.html b/backlog-mermaid.html
new file mode 100644
index 000000000..b3885bfd5
--- /dev/null
+++ b/backlog-mermaid.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+ 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. 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
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 our forumopen 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
You can change the environment lighting and skybox using the Viewport shading options. Assign a cubemap to use for lighting or the background skybox. You can adjust the strength or blur to modify the appearance to your liking.
Note: To also see the skybox cubemap in the browser increase the World Opacity to 1.
Note: Alternatively you can enable the Scene World setting in the Viewport Shading tab to use the environment texture assigned in the blender world settings.
Alternatively if you don't want to see the cubemap as a background add a Camera component to your Blender Camera and change clearFlags: SolidColor - note that the Camera backgroundBlurriness and backgroundIntensity settings override the Viewport shading settings.
# Add your custom HDRi / EXR environment lighting and skybox
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/ericcraft-mh/index.html b/community/contributions/ericcraft-mh/index.html
new file mode 100644
index 000000000..c09e7a64d
--- /dev/null
+++ b/community/contributions/ericcraft-mh/index.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+ Documentation
+
+
+
+
+
SCNPlane is vertically oriented in its local coordinate space, but ARImageAnchor assumes the image is horizontal in its local space, so rotate the plane to match.
SCNPlane is vertically oriented in its local coordinate space, but ARImageAnchor assumes the image is horizontal in its local space, so rotate the plane to match.
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.
Custom VR Button, that appears only on headsets and not on mobile phones
I combined two checks - Needle's check to see if it's a mobile device (this way, you can exclude desktops), and then a second check that uses user agent info to see if it's one of the most common mobile systems. Result: the button does not appear on mobile phones, but it is visible on Quest and Pico ๐
P.S: I am using Svelte for 2D UI handling.
import{ isMobileDevice, NeedleXRSession }from"@needle-tools/engine";
+
+...
+
+// check if this is a mobile phone
+functionisMobilePhone(){
+ return/Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+}
+
+...
+
+// show the button, if the device is not a mobile phone and VR is supported
+{#ifisMobileDevice()&&!isMobilePhone()&& $haveVR }
+ <VrButton buttonFunction={()=>StartVR()}/>
+{/if}
+
Custom VR Button, that appears only on headsets and not on mobile phones
I combined two checks - Needle's check to see if it's a mobile device (this way, you can exclude desktops), and then a second check that uses user agent info to see if it's one of the most common mobile systems. Result: the button does not appear on mobile phones, but it is visible on Quest and Pico ๐
P.S: I am using Svelte for 2D UI handling.
import{ isMobileDevice, NeedleXRSession }from"@needle-tools/engine";
+
+...
+
+// check if this is a mobile phone
+functionisMobilePhone(){
+ return/Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+}
+
+...
+
+// show the button, if the device is not a mobile phone and VR is supported
+{#ifisMobileDevice()&&!isMobilePhone()&& $haveVR }
+ <VrButton buttonFunction={()=>StartVR()}/>
+{/if}
+
Set fallback material for USDZ exporter
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
Microphone access in a browser window (and streamed playback)
A simple script to show how to request access to, then access a microphone device and also play back the audio stream to debug it. A useful starting point for making an experience revolving around microphone access.
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
Microphone access in a browser window (and streamed playback)
A simple script to show how to request access to, then access a microphone device and also play back the audio stream to debug it. A useful starting point for making an experience revolving around microphone access.
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. Many of them map to components and functionality in Unity, Blender or other integrations.
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 effects using a Volume in Unity is only supported with URP
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 (Legacy)
Render Text using Unity's UI Text component. 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. Note: In Unity make sure to use the Legacy/Text component (TextMeshPro is not supported at the moment)
Button
Receives click events - use the onClick event to react to it. It can be added too 3D scene objects as well. Note: Make sure to use the Legacy/Text component in the Button (or create the Button via the UI/Legacy/Button Unity context menu since TextMeshPro is not supported at the moment)
Image
Renders a sprite image
RawImage
Renders a texture
InputField
Allows text input
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.
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
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 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:
Since Needle Engine 3.36 we automatically generate LOD meshes and switch between them at runtime. LODs are loaded on demand and only when needed so so this feature both reduces your loading time as well as performance.
Key Beneftis
Faster initial loading time
Faster rendering time due to less vertices on screen on average
Faster raycasting due to the use of LOD meshes
You can either disable LOD generation for your whole project in the Progressive Loading Settings component or in the Mesh Importer settings.
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 by deploying 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.
To enable gzip compression on your FTP server you can create a file named .htaccess in the directory you want to upload to (or a parent directory). Insert the following code into your .htaccess file and save/upload it to your server:
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.
In Unity open File/Build Settings and select Needle Engine for options:
To build your web project for uploading to any 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.
To locally preview your final build you can use the Preview Build button at the bottom of the window. This button will first perform a regular build and then start a local server in the directory with the final files so you can see what you get once you upload these files to your webserver.
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).
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..f50519ee4
--- /dev/null
+++ b/everywhere-actions.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+ Everywhere Actions | 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:
Open Edit/Project Settings/Needle to get the Needle Engine plugin settings. At the top of the window you'll find fields for entering your license information.
Email - Enter the email you purchased the license with
Invoice ID - Enter one of the invoice ids that you received by email
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.
Needle Engine is a fully fledged 3D engine that runs in the browser. It comes with all the features you'd expect from a modern 3D engine, and more. If you haven't yet, take a look at our Homepageopen in new window and our Samples and Showcaseopen in new window.
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.
With Needle Engine, you can create fully interactive 3D websites. They can be deployed anywhere on the web and get optimized automatically by the Needle Engine Build Pipeline.
Needle Engine is available as a download for Unity, for Blender, and for web projects without an editor integration.
Drop the downloaded .unitypackage file into a Unity project and confirm that you want to import it.
Wait a moment 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. You can safely close that window.
Explore Samples โ Select the menu option Needle Engine > Explore Samples to view, open and modify all available sample scenesopen in new window.
Video Tutorial: 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. We recommend the Collaborative Sandboxopen in new window template which is a great way to get started with interactivity, multiplayer, and adding assets.
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
You can work directly with Needle Engine without using any Integration. Needle Engine uses three.jsopen in new window as scene graph and rendering backend, so all functionality from three.js is available in Needle as well.
You can install Needle Engine from npm by running:
Needle Engine uses Node.js to manage, preview and build the web app that you are creating locally on your computer. It is also used for uploading (deploying) your website to the internet. Please download it from the official 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.
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. Our default template uses the popular viteopen in new window as bundler. From there, you can add vue, svelte, nuxt, react, react-three-fiber or other frameworks, and we have samples for a lot of them. You can also integrate other bundlers, or use none at all โ just plain HTML and Javascript.
Here's some example tech stacks that are possible and that we use Needle Engine with:
Vite + HTML โ This 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
We support easily creating a Progressive Web App (PWA) directly from our vite template. PWAs are web applications that load like regular web pages or websites but can offer user functionality such as working offline, push notifications, and device hardware access traditionally available only to native mobile applications.
By default, PWAs created with Needle have offline support, and can optionally refresh automatically when you publish a new version of your app.
Modify vite.config.js as seen below. Make sure to pass the same pwaOptions object to both needlePlugins and VitePWA.
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,{pwa: pwaOptions }),
+ VitePWA(pwaOptions),
+ ],
+ // the rest of your vite config...
+
All assets are cached by default
Note that by default, all assets in your build folder are added the PWA precache โ for large applications with many dynamic assets, this may not be what you want (imagine the YouTube PWA caching all videos once a user opens the app!). See More PWA Options for how to customize this behavior.
To test your PWA, deploy the page, for example using the DeployToFTP component. Then, open the deployed page in a browser and check if the PWA features work as expected:
PWAs use Service Workers to cache resources and provide offline support. Service Workers are somewhat harder to use during development, and typically are only enabled for builds (e.g. when you use a DeployTo... component).
You can enable PWA support for development by adding the following to the options object in your vite.config.js.
const pwaOptions ={
+ // Note: PWAs behave different in dev mode.
+ // Make sure to verify the behaviour in production builds!
+ devOptions:{
+ enabled:true,
+ }
+};
+
Please note that PWAs in development mode do not support offline usage โ trying it may result in unexpected behavior.
Websites typically show new or updated content on page refresh.
In some situations, you may want the page to refresh and reload automatically when a new version has been published โ such as in a museum, trade show, public display, or other long-running scenarios.
To enable automatic updates, set the updateInterval property in the pwaOptions object to a duration (in milliseconds) in which the app should check for updates. If an update is detected, the page will reload automatically.
It's not recommended to use automatic reloads in applications where users are interacting with forms or other data that could be lost on a reload. For these applications, showing a reload prompt is recommended. See the Vite PWA plugin documentationopen in new window for more information on how to implement a reload prompt instead of automatic reloading.
Since Needle uses the Vite PWA pluginopen in new window under the hood, you can use all options and hooks provided by that. For example, you can provide a partial manifest with a custom app title or theme color:
const pwaOptions ={
+ // manifest options provided here will override the defaults
+ manifest:{
+ name:"My App",
+ short_name:"My App",
+ theme_color:"#B2D464",
+ }
+};
+
For complex requirements like partial caching, custom service workers or different update strategies, you can remove the { pwa: pwaOptions } option from needlePlugins and add PWA functionality directly through the Vite PWA plugin.
# 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. Work on your machine and deploy anywhere. Needle Engine 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. These Integrations allow you to use editor features for creating models, authoring materials, animating and sequencing animations, baking 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..f54483c5e
--- /dev/null
+++ b/modules.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+ Additional Modules | 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 skybox image (background image) or a preset string: studio, blurred-skybox, quicklook, quicklook-ar
environment-image
optional, URL to a environment image (environment light) or a preset string: studio, blurred-skybox, quicklook, quicklook-ar
contactshadows
optional, render contact shadows
tone-mapping
optional, supported values are none, linear, neutral, agx
tone-mapping-exposure
optional number e.g. increase exposure with tone-mapping-exposure="1.5", requires tone-mapping to be set
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
+ }
+}
+
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 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(cb, options)
Called when a new context is initialized (before the first frame)
onClear(cb, options)
Register a callback before the engine context is cleared
onDestroy(cb, options)
Register a callback in the engine before the context is destroyed
onStart(cb, options)
Called directly after components start at the beginning of a frame
// this can be put into e.g. main.ts or a svelte component (similar to onMount)
+import{ onUpdate, onBeforeRender }from"@needle-tools/engine"
+onUpdate((ctx: Context)=>{
+ // do something... e.g. access the scene via ctx.scene
+ console.log("UPDATE", ctx.time.frame);
+});
+
+onBeforeRender((ctx: Context)=>{
+ // this event is only called once because of the { once: true } argument
+ console.log("ON BEFORE RENDER", ctx.time.frame);
+},{ once:true});
+
+// Every event hook returns a method to unsubscribe from the event
+const unsubscribe =onAfterRender((ctx: Context)=>{
+ console.log("ON AFTER RENDER", ctx.time.frame);
+});
+// Unsubscribe from the event at any time
+setTimeout(()=>unsubscribe(),1000);
+
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..2b5e8f448
--- /dev/null
+++ b/showcase-monsterhands.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+ Monster Hands ๐ | 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 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 tools like Unity or Blender into spatial web development powerhouses โ adding glTF assets to the typical HTML, CSS, JavaScript 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
My workflow has been optimized 10X ever since i started using needle โ Yuzu
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.
+
+
+
diff --git a/testing/switch-to-mkcert.webp b/testing/switch-to-mkcert.webp
new file mode 100644
index 000000000..dee32c704
Binary files /dev/null and b/testing/switch-to-mkcert.webp differ
diff --git a/vanilla-js.html b/vanilla-js.html
new file mode 100644
index 000000000..8e24b7001
--- /dev/null
+++ b/vanilla-js.html
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+ Using Needle Engine directly from HTML | Documentation
+
+
+
+
+
While our default template uses viteopen in new window as powerful bundler, Needle Engine can also be used directly with vanilla Javascript โ without using any bundler.
Then open this index.html with the live-server and navigate to http://localhost:5500/index.html in your web browser.
TIP
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.
Theoretically all WebXR-capable devices and browsers should be supported. That being said, we've tested the following configurations:
Tested VR Device
Browser
Notes
Apple Vision Pro
โ๏ธ Safari Browser
hand tracking, support for transient pointer
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 3
โ๏ธ Meta Browser
hand tracking, support for sessiongranted1, passthrough, depth sensing, mesh tracking
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)
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.
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, 2 and 3 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: