├── .eslintrc.json ├── .gitignore ├── .nojekyll ├── CNAME ├── README.md ├── constants.js ├── constants.json ├── constants.module.js ├── contract_templates ├── cryptovoxels.js ├── loomlock.js └── moreloot.js ├── contracts ├── cryptovoxels.js ├── loomlock.js └── moreloot.js ├── index.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ └── [...slug].js └── index.js ├── plugins └── rollup.js ├── scripts └── compile.js ├── type_templates ├── background.js ├── fog.js ├── gif.js ├── glb.js ├── glbb.js ├── gltj.js ├── group.js ├── html.js ├── image.js ├── light.js ├── lore.js ├── mob.js ├── npc.js ├── quest.js ├── react.js ├── rendersettings.js ├── scn.js ├── spawnpoint.js ├── text.js ├── vircadia.js ├── vox.js ├── vrm.js └── wind.js ├── types ├── background.js ├── directory.js ├── fog.js ├── gif.js ├── glb.js ├── glbb.js ├── gltj.js ├── group.js ├── html.js ├── image.js ├── jsx.js ├── light.js ├── lore.js ├── metaversefile.js ├── mob.js ├── npc.js ├── quest.js ├── react.js ├── rendersettings.js ├── scn.js ├── spawnpoint.js ├── text.js ├── vircadia.js ├── vox.js ├── vrm.js └── wind.js └── util.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webaverse/totum/e72858b81c07db1677b9807c255170dd070887e1/.nojekyll -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | metaversefile.org -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Totum 2 | 3 | ## Overview 4 | 5 | This library lets you compile a URL (https://, ethereum://, and more) into a THREE.js app representing it, written against the Metaversefile API. 6 | 7 | You can use this library to translate your avatars, models, NFTs, web pages (and more) into a collection of `import()`-able little web apps that interoperate with each other. 8 | 9 | Totum is intended to be driven by a server framework (like vite.js/rollup.js), and game engine client (like Webaverse) to provide a complete immersive world (or metaverse) to the user. 10 | 11 | It is easy to define your own data types and token interpretations by writing your own app template. If you would like to support a new file format or Ethereum Token, we would appreciate a PR. 12 | 13 | Although this library does not provide game engine facilities, the API is designed to be easy to hook into game engines, and to be easy to drive using AIs like OpenAI's Codex. 14 | 15 | --- 16 | 17 | ## Usage 18 | 19 | ```js 20 | 21 | let object; 22 | try { 23 | object = await metaversefileApi.load(url); 24 | } catch (err) { 25 | console.warn(err); 26 | } 27 | return object; 28 | 29 | ``` 30 | 31 | ### Inputs 32 | * url: {URL of the asset that can be downloadable by the screenshot system} **[Required]** 33 | 34 | ### Returns 35 | * Promise: 36 | 37 | ### Output 38 | * Object of application 39 | 40 | ### Supported Assets 41 | * `VRM` 42 | * `VOX` 43 | * `JS` 44 | * `SCN` 45 | * `IMAGE` 46 | * `HTML` 47 | * `GLB` 48 | * `GIF` 49 | 50 | ## Motivations 51 | 52 | - A system which takes any URL (or token) and manifests it as an object in a 3D MMO 53 | - Totum transmutes data on the backend, serving composable little WASM+JS apps your browser can import() 54 | - Object description language (`.metaversefile`) to declare game presentation. Sword? Wearable loot? Pet is aggro? Think CSS/JSON for the metaverse. 55 | - Totum works completely permissionlessly. It provides a virtual lens into data, and you control the lens. 56 | - Totum supports declaring per-object components, which can have gameplay effects 57 | - Pure open source web tech 58 | - Moddable; make your metaverse look and work the way you want 59 | - Totum integrates into game engines, which provide the game. 60 | - Totum works with 2D ($1K jpg) and 3D ($15K fbx) assets. 61 | - Totum accepts PRs to improve the resolution of the metaverse 62 | - It's called Totum because it snaps together your objects into a total experience 63 | 64 | --- 65 | ## Architecture 66 | 67 | ### Flow Diagram 68 | 69 | ![Totum diagram 02](https://user-images.githubusercontent.com/51108458/144339720-354aa56d-aa61-4e96-b49c-bf9e652d1f48.png) 70 | 71 | 72 | 73 | --- 74 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | export const contractNames = { 2 | "0x79986af15539de2db9a5086382daeda917a9cf0c": "cryptovoxels", 3 | "0x1dfe7ca09e99d10835bf73044a23b73fc20623df": "moreloot", 4 | "0x1d20a51f088492a0f1c57f047a9e30c9ab5c07ea": "loomlock" 5 | }; -------------------------------------------------------------------------------- /constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractNames": { 3 | "0x79986af15539de2db9a5086382daeda917a9cf0c": "cryptovoxels", 4 | "0x1dfe7ca09e99d10835bf73044a23b73fc20623df": "moreloot", 5 | "0x1d20a51f088492a0f1c57f047a9e30c9ab5c07ea": "loomlock" 6 | } 7 | } -------------------------------------------------------------------------------- /constants.module.js: -------------------------------------------------------------------------------- 1 | import constants from './constants.json'; 2 | export default constants; -------------------------------------------------------------------------------- /contract_templates/cryptovoxels.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import pako from 'pako'; 3 | import metaversefile from 'metaversefile'; 4 | const {useApp, useLoaders, useCleanup, usePhysics} = metaversefile; 5 | 6 | function convertDataURIToBinary(base64) { 7 | var raw = window.atob(base64); 8 | var rawLength = raw.length; 9 | var array = new Uint8Array(new ArrayBuffer(rawLength)); 10 | 11 | for (let i = 0; i < rawLength; i++) { 12 | array[i] = raw.charCodeAt(i); 13 | } 14 | return array; 15 | } 16 | 17 | const _getParcel = (x, z, parcels) => parcels.find(parcel => { 18 | return x >= parcel.x1 && x < parcel.x2 && z >= parcel.z1 && z < parcel.z2; 19 | }); 20 | const _getContent = async (id, hash) => { 21 | const res = await fetch('https://https-js-cryptovoxels-com.proxy.webaverse.com/grid/parcels/' + id + '/at/' + hash); 22 | const j = await res.json(); 23 | const {parcel} = j; 24 | return parcel; 25 | }; 26 | 27 | const _getTextureMaterial = u => { 28 | const img = new Image(); 29 | img.onload = () => { 30 | texture.needsUpdate = true; 31 | }; 32 | img.onerror = err => { 33 | console.warn(err); 34 | }; 35 | img.crossOrigin = 'Anonymous'; 36 | img.src = u; 37 | const texture = new THREE.Texture(img); 38 | const material = new THREE.MeshBasicMaterial({ 39 | map: texture, 40 | side: THREE.DoubleSide, 41 | // transparent: true, 42 | }); 43 | material.polygonOffset = true; 44 | material.polygonOffsetFactor = -1.0; 45 | material.polygonOffsetUnits = -4.0; 46 | return material; 47 | }; 48 | const _getTextureMaterialCached = (() => { 49 | const cache = {}; 50 | return u => { 51 | let entry = cache[u]; 52 | if (!entry) { 53 | entry = _getTextureMaterial(u); 54 | cache[u] = entry; 55 | } 56 | return entry; 57 | }; 58 | })(); 59 | const zoom = 8; 60 | const tileRange = 4; 61 | const centerTile = 128; 62 | const tilePixelWidth = 256; 63 | const numTiles = tileRange*2; 64 | const mapSize = 1000*2*(1 + 1/numTiles); 65 | // const tileVoxelWidth = mapSize/4/numTiles; 66 | 67 | const _loadVox = async u => { 68 | let o = await new Promise((accept, reject) => { 69 | const {voxLoader} = useLoaders(); 70 | voxLoader.load(u, accept, function onprogress() {}, reject); 71 | }); 72 | const {geometry} = o; 73 | const positions = geometry.attributes.position.array; 74 | const normals = geometry.attributes.normal.array; 75 | // const indices = geometry.index.array; 76 | for (let i = 0; i < positions.length; i += 3) { 77 | positions[i] = -positions[i]; 78 | 79 | normals[i] *= -1; 80 | normals[i+1] *= -1; 81 | normals[i+2] *= -1; 82 | } 83 | const indices = new Uint32Array(positions.length/3); 84 | for (let i = 0; i < indices.length; i += 3) { 85 | indices[i] = i; 86 | indices[i+1] = i+2; 87 | indices[i+2] = i+1; 88 | } 89 | geometry.setIndex(new THREE.BufferAttribute(indices, 1)); 90 | return o; 91 | }; 92 | const _loadVoxCached = (() => { 93 | const cache = {}; 94 | return async u => { 95 | let entry = cache[u]; 96 | if (!entry) { 97 | entry = await _loadVox(u); 98 | cache[u] = entry; 99 | } 100 | return entry.clone(); 101 | }; 102 | })(); 103 | 104 | function GetVoxel(field, width, height, depth, x, y, z) { 105 | if (x < 0 || y < 0 || z < 0 || x >= width * 2 || y >= height * 2 || z >= depth * 2) 106 | { 107 | return 0; 108 | } 109 | else 110 | { 111 | const index = z + y*(depth*2) + x*(depth*2)*(height*2); 112 | return field[index]; 113 | } 114 | } 115 | function AddUvs(index, transparent, uvs, uvIndex) 116 | { 117 | if (transparent) 118 | { 119 | uvs[uvIndex++] = 0; 120 | uvs[uvIndex++] = 0; 121 | uvs[uvIndex++] = 1; 122 | uvs[uvIndex++] = 0; 123 | uvs[uvIndex++] = 0; 124 | uvs[uvIndex++] = 1; 125 | uvs[uvIndex++] = 1; 126 | uvs[uvIndex++] = 1; 127 | 128 | return uvIndex; 129 | } 130 | 131 | const s = 1.0 / 16 / 128 * 128; 132 | 133 | // long textureIndex = index - (1 << 15); 134 | const textureIndex = index % 16; 135 | // const textureIndex = index & 15; 136 | 137 | // if (textureIndex >= 32) { 138 | // // inverted 139 | // textureIndex = (textureIndex % 32) * 2 + 1 140 | // } else { 141 | // textureIndex = (textureIndex % 32) * 2 142 | // } 143 | 144 | let x = 1.0 / 4 * ((textureIndex % 4) + 0.5); 145 | let y = 1.0 / 4 * (Math.floor(textureIndex / 4.0) + 0.5); 146 | 147 | y = 1.0 - y; 148 | 149 | uvs[uvIndex++] = x - s; 150 | uvs[uvIndex++] = y - s; 151 | uvs[uvIndex++] = x + s; 152 | uvs[uvIndex++] = y - s; 153 | uvs[uvIndex++] = x - s; 154 | uvs[uvIndex++] = y + s; 155 | uvs[uvIndex++] = x + s; 156 | uvs[uvIndex++] = y + s; 157 | 158 | return uvIndex; 159 | } 160 | const _colorTable = [ 161 | new THREE.Color(0xffffff), 162 | new THREE.Color(0x888888), 163 | new THREE.Color(0x000000), 164 | new THREE.Color(0xff71ce), 165 | new THREE.Color(0x01cdfe), 166 | new THREE.Color(0x05ffa1), 167 | new THREE.Color(0xb967ff), 168 | new THREE.Color(0xfffb96), 169 | ]; 170 | const white = new THREE.Color(0xFFFFFF); 171 | const red = new THREE.Color(0xFF0000); 172 | 173 | function UIntToColor(color) 174 | { 175 | //string binary = Convert.ToString(color, 2); 176 | //binary = binary.Remove(binary.Length - 2 - 1, 2); 177 | //uint newValue = Convert.ToUInt32(binary, 2); 178 | //uint c = color;// - (byte) color;// (byte)(color & ~(1 << 4)); 179 | // string debug = ""; 180 | if (color >= 32768) 181 | { 182 | color -= 32768; 183 | // debug += "big "; 184 | } 185 | 186 | if (color === 0) 187 | { 188 | return white; 189 | } 190 | 191 | if (color > 32) 192 | { 193 | //uint colorIndex = (uint)Mathf.Floor(color / 32.0f); 194 | //Debug.Log(color-32); 195 | 196 | const index = Math.floor(color / 32); 197 | if (index >= _colorTable.length) { 198 | return red; 199 | } 200 | return _colorTable[index]; 201 | } 202 | return white; 203 | 204 | } 205 | 206 | function IsTransparent(index) { 207 | return index <= 2; 208 | } 209 | function IsGeometry(index, transparent) { 210 | return transparent ? index == 2 : index > 2; 211 | } 212 | function GenerateField(field, width, height, depth, transparent) { 213 | const newVertices = new Float32Array(1024 * 1024); 214 | const newUV = new Float32Array(1024 * 1024); 215 | const indices = new Uint32Array(1024 * 1024); 216 | const newColor = new Float32Array(1024 * 1024); 217 | 218 | let vertexIndex = 0; 219 | let uvIndex = 0; 220 | let indexIndex = 0; 221 | let colorIndex = 0; 222 | 223 | for (let x = -1; x < width * 2; x++) 224 | { 225 | for (let y = -1; y < height * 2; y++) 226 | { 227 | for (let z = -1; z < depth * 2; z++) 228 | { 229 | const i = GetVoxel(field, width, height, depth, x, y, z); 230 | const nX = GetVoxel(field, width, height, depth, x + 1, y, z); 231 | const nY = GetVoxel(field, width, height, depth, x, y + 1, z); 232 | const nZ = GetVoxel(field, width, height, depth, x, y, z + 1); 233 | 234 | if (IsGeometry(i, transparent) !== IsGeometry(nX, transparent)) 235 | { 236 | const v = vertexIndex/3; 237 | 238 | /* newVertices.Add(new Vector3(x + 1, y + 1, z)); 239 | newVertices.Add(new Vector3(x + 1, y + 1, z + 1)); 240 | newVertices.Add(new Vector3(x + 1, y, z)); 241 | newVertices.Add(new Vector3(x + 1, y, z + 1)); */ 242 | 243 | newVertices[vertexIndex++] = x + 1; 244 | newVertices[vertexIndex++] = y + 1; 245 | newVertices[vertexIndex++] = z; 246 | newVertices[vertexIndex++] = x + 1; 247 | newVertices[vertexIndex++] = y + 1; 248 | newVertices[vertexIndex++] = z + 1; 249 | newVertices[vertexIndex++] = x + 1; 250 | newVertices[vertexIndex++] = y; 251 | newVertices[vertexIndex++] = z; 252 | newVertices[vertexIndex++] = x + 1; 253 | newVertices[vertexIndex++] = y; 254 | newVertices[vertexIndex++] = z + 1; 255 | 256 | if (i > nX) 257 | { 258 | indices[indexIndex++] = v + 0; 259 | indices[indexIndex++] = v + 1; 260 | indices[indexIndex++] = v + 2; 261 | 262 | indices[indexIndex++] = v + 1; 263 | indices[indexIndex++] = v + 3; 264 | indices[indexIndex++] = v + 2; 265 | 266 | uvIndex = AddUvs(i, transparent, newUV, uvIndex); 267 | 268 | const c = UIntToColor(i); 269 | c.toArray(newColor, colorIndex); 270 | colorIndex += 3; 271 | c.toArray(newColor, colorIndex); 272 | colorIndex += 3; 273 | c.toArray(newColor, colorIndex); 274 | colorIndex += 3; 275 | c.toArray(newColor, colorIndex); 276 | colorIndex += 3; 277 | } 278 | else 279 | { 280 | indices[indexIndex++] = v + 2; 281 | indices[indexIndex++] = v + 1; 282 | indices[indexIndex++] = v + 0; 283 | 284 | indices[indexIndex++] = v + 2; 285 | indices[indexIndex++] = v + 3; 286 | indices[indexIndex++] = v + 1; 287 | 288 | uvIndex = AddUvs(nX, transparent, newUV, uvIndex); 289 | 290 | const c = UIntToColor(nX); 291 | c.toArray(newColor, colorIndex); 292 | colorIndex += 3; 293 | c.toArray(newColor, colorIndex); 294 | colorIndex += 3; 295 | c.toArray(newColor, colorIndex); 296 | colorIndex += 3; 297 | c.toArray(newColor, colorIndex); 298 | colorIndex += 3; 299 | } 300 | } 301 | 302 | if (IsGeometry(i, transparent) !== IsGeometry(nY, transparent)) 303 | { 304 | const v = vertexIndex/3; 305 | 306 | /* newVertices.Add(new Vector3(x, y + 1, z)); 307 | newVertices.Add(new Vector3(x + 1, y + 1, z)); 308 | newVertices.Add(new Vector3(x, y + 1, z + 1)); 309 | newVertices.Add(new Vector3(x + 1, y + 1, z + 1)); */ 310 | 311 | newVertices[vertexIndex++] = x; 312 | newVertices[vertexIndex++] = y + 1; 313 | newVertices[vertexIndex++] = z; 314 | newVertices[vertexIndex++] = x + 1; 315 | newVertices[vertexIndex++] = y + 1; 316 | newVertices[vertexIndex++] = z; 317 | newVertices[vertexIndex++] = x; 318 | newVertices[vertexIndex++] = y + 1; 319 | newVertices[vertexIndex++] = z + 1; 320 | newVertices[vertexIndex++] = x + 1; 321 | newVertices[vertexIndex++] = y + 1; 322 | newVertices[vertexIndex++] = z + 1; 323 | 324 | if (i < nY) 325 | { 326 | indices[indexIndex++] = v + 0; 327 | indices[indexIndex++] = v + 1; 328 | indices[indexIndex++] = v + 2; 329 | 330 | indices[indexIndex++] = v + 1; 331 | indices[indexIndex++] = v + 3; 332 | indices[indexIndex++] = v + 2; 333 | 334 | uvIndex = AddUvs(nY, transparent, newUV, uvIndex); 335 | 336 | const c = UIntToColor(nY); 337 | c.toArray(newColor, colorIndex); 338 | colorIndex += 3; 339 | c.toArray(newColor, colorIndex); 340 | colorIndex += 3; 341 | c.toArray(newColor, colorIndex); 342 | colorIndex += 3; 343 | c.toArray(newColor, colorIndex); 344 | colorIndex += 3; 345 | } 346 | else 347 | { 348 | indices[indexIndex++] = v + 2; 349 | indices[indexIndex++] = v + 1; 350 | indices[indexIndex++] = v + 0; 351 | 352 | indices[indexIndex++] = v + 2; 353 | indices[indexIndex++] = v + 3; 354 | indices[indexIndex++] = v + 1; 355 | 356 | uvIndex = AddUvs(i, transparent, newUV, uvIndex); 357 | 358 | const c = UIntToColor(i); 359 | c.toArray(newColor, colorIndex); 360 | colorIndex += 3; 361 | c.toArray(newColor, colorIndex); 362 | colorIndex += 3; 363 | c.toArray(newColor, colorIndex); 364 | colorIndex += 3; 365 | c.toArray(newColor, colorIndex); 366 | colorIndex += 3; 367 | } 368 | 369 | } 370 | 371 | if (IsGeometry(i, transparent) !== IsGeometry(nZ, transparent)) 372 | { 373 | const v = vertexIndex/3; 374 | 375 | /* newVertices.Add(new Vector3(x, y, z + 1)); 376 | newVertices.Add(new Vector3(x + 1, y, z + 1)); 377 | newVertices.Add(new Vector3(x, y + 1, z + 1)); 378 | newVertices.Add(new Vector3(x + 1, y + 1, z + 1)); */ 379 | 380 | newVertices[vertexIndex++] = x; 381 | newVertices[vertexIndex++] = y; 382 | newVertices[vertexIndex++] = z + 1; 383 | newVertices[vertexIndex++] = x + 1; 384 | newVertices[vertexIndex++] = y; 385 | newVertices[vertexIndex++] = z + 1; 386 | newVertices[vertexIndex++] = x; 387 | newVertices[vertexIndex++] = y + 1; 388 | newVertices[vertexIndex++] = z + 1; 389 | newVertices[vertexIndex++] = x + 1; 390 | newVertices[vertexIndex++] = y + 1; 391 | newVertices[vertexIndex++] = z + 1; 392 | 393 | if (i > nZ) 394 | { 395 | indices[indexIndex++] = v + 0; 396 | indices[indexIndex++] = v + 1; 397 | indices[indexIndex++] = v + 2; 398 | 399 | indices[indexIndex++] = v + 1; 400 | indices[indexIndex++] = v + 3; 401 | indices[indexIndex++] = v + 2; 402 | 403 | uvIndex = AddUvs(i, transparent, newUV, uvIndex); 404 | 405 | const c = UIntToColor(i); 406 | c.toArray(newColor, colorIndex); 407 | colorIndex += 3; 408 | c.toArray(newColor, colorIndex); 409 | colorIndex += 3; 410 | c.toArray(newColor, colorIndex); 411 | colorIndex += 3; 412 | c.toArray(newColor, colorIndex); 413 | colorIndex += 3; 414 | } 415 | else 416 | { 417 | indices[indexIndex++] = v + 2; 418 | indices[indexIndex++] = v + 1; 419 | indices[indexIndex++] = v + 0; 420 | 421 | indices[indexIndex++] = v + 2; 422 | indices[indexIndex++] = v + 3; 423 | indices[indexIndex++] = v + 1; 424 | 425 | uvIndex = AddUvs(nZ, transparent, newUV, uvIndex); 426 | 427 | const c = UIntToColor(nZ); 428 | c.toArray(newColor, colorIndex); 429 | colorIndex += 3; 430 | c.toArray(newColor, colorIndex); 431 | colorIndex += 3; 432 | c.toArray(newColor, colorIndex); 433 | colorIndex += 3; 434 | c.toArray(newColor, colorIndex); 435 | colorIndex += 3; 436 | } 437 | } 438 | } 439 | } 440 | } 441 | for (let i = 0; i < vertexIndex; i += 3) { 442 | newVertices[i] = -newVertices[i]; 443 | } 444 | for (let i = 0; i < indexIndex; i += 3) { 445 | const a = indices[i+1]; 446 | indices[i+1] = indices[i]; 447 | indices[i] = a; 448 | } 449 | const geometry = new THREE.BufferGeometry(); 450 | geometry.setAttribute('position', new THREE.BufferAttribute(newVertices.subarray(0, vertexIndex), 3)); 451 | geometry.setAttribute('uv', new THREE.BufferAttribute(newUV.subarray(0, uvIndex), 2)); 452 | geometry.setAttribute('color', new THREE.BufferAttribute(newColor.subarray(0, colorIndex), 3)); 453 | geometry.setIndex(new THREE.BufferAttribute(indices.subarray(0, indexIndex), 1)); 454 | return geometry; 455 | } 456 | const _getCoord = (x, z) => parcels.find(parcel); 457 | 458 | export default () => { 459 | const app = useApp(); 460 | const physics = usePhysics(); 461 | 462 | const contractAddress = '${this.contractAddress}'; 463 | const tokenId = parseInt('${this.tokenId}', 10); 464 | console.log('got token id', tokenId); 465 | 466 | const physicsIds = []; 467 | (async () => { 468 | const parcels = await (async () => { 469 | const res = await fetch('https://https-www-cryptovoxels-com.proxy.webaverse.com/api/parcels/cached.json'); 470 | const j = await res.json(); 471 | const {parcels} = j; 472 | return parcels; 473 | })(); 474 | const parcel = parcels.find(parcel => parcel.id === tokenId); 475 | // console.log('got parcels', parcels, parcel); 476 | 477 | const imageGeometry = new THREE.PlaneBufferGeometry(2, 2) 478 | .applyMatrix4(new THREE.Matrix4().makeScale(-1, 1, 1)); 479 | for (let i = 0; i < imageGeometry.index.array.length; i += 3) { 480 | const a = imageGeometry.index.array[i+1]; 481 | imageGeometry.index.array[i+1] = imageGeometry.index.array[i]; 482 | imageGeometry.index.array[i] = a; 483 | } 484 | 485 | const tileMaterial = new THREE.MeshBasicMaterial({ 486 | color: 0x0000FF, 487 | transparent: true, 488 | opacity: 0.5, 489 | side: THREE.DoubleSide, 490 | }); 491 | 492 | // const parcel = _getParcel(x, z, parcels); 493 | // console.log('load coord', x, z, parcel); 494 | 495 | const object = new THREE.Object3D(); 496 | { 497 | const {id, hash} = parcel; 498 | const content = await _getContent(id, hash); 499 | 500 | let {voxels, palette, tileset, features, x1, x2, y1, y2, z1, z2} = content; 501 | if (tileset) { 502 | tileset = 'https://https-cdn-cryptovoxels-com.proxy.webaverse.com' + tileset; 503 | } else { 504 | tileset = 'https://https-www-cryptovoxels-com.proxy.webaverse.com/textures/atlas-ao.png'; 505 | } 506 | const w = x2 - x1; 507 | const h = y2 - y1; 508 | const d = z2 - z1; 509 | const field = new Uint16Array(pako.inflate(convertDataURIToBinary(voxels)).buffer); 510 | 511 | // object.position.x = x1*2; 512 | // object.position.z = -z1*2; 513 | object.rotation.order = 'YXZ'; 514 | object.rotation.y = Math.PI; 515 | 516 | console.log('got content', content, w, h, d, w*h*d); 517 | const solidGeometry = GenerateField(field, w, h, d, false); 518 | const transparentGeometry = GenerateField(field, w, h, d, true); 519 | console.log('got geometries', {solidGeometry, transparentGeometry}); 520 | 521 | const img = new Image(); 522 | img.crossOrigin = 'Anonymous'; 523 | img.src = tileset; 524 | img.onload = () => { 525 | texture.needsUpdate = true; 526 | }; 527 | img.onerror = err => { 528 | console.warn(err.stack); 529 | }; 530 | const texture = new THREE.Texture(img); 531 | 532 | { 533 | solidGeometry.computeVertexNormals(); 534 | const material = new THREE.MeshPhongMaterial({ 535 | // color: 0xFF0000, 536 | map: texture, 537 | vertexColors: true, 538 | }); 539 | const mesh = new THREE.Mesh(solidGeometry, material); 540 | mesh.frustumCulled = false; 541 | object.add(mesh); 542 | } 543 | { 544 | transparentGeometry.computeVertexNormals(); 545 | const material = new THREE.MeshPhongMaterial({ 546 | // map: texture, 547 | // color: 0xFFFFFF, 548 | vertexColors: true, 549 | transparent: true, 550 | opacity: 0.5, 551 | }); 552 | const mesh = new THREE.Mesh(transparentGeometry, material); 553 | mesh.frustumCulled = false; 554 | object.add(mesh); 555 | } 556 | for (const feature of features) { 557 | // console.log('got feature', feature); 558 | 559 | try { 560 | const {type} = feature; 561 | switch (type) { 562 | case 'image': { 563 | const {url, position, rotation, scale} = feature; 564 | const u = new URL(url); 565 | console.log('got u 1', u.href); 566 | u.host = u.protocol.replace(/:/g, '-') + u.host.replace(/\\-/g, '--').replace(/\\./g, '-') + '.proxy.webaverse.com'; 567 | console.log('got u 2', u.href); 568 | const geometry = imageGeometry; 569 | const material = _getTextureMaterialCached(u.href); 570 | const mesh = new THREE.Mesh(geometry, material); 571 | mesh.frustumCulled = false; 572 | mesh.position.fromArray(position); 573 | mesh.position.x *= -1; 574 | mesh.position.multiplyScalar(2); 575 | mesh.position.x -= w - 0.5; 576 | mesh.position.y += 0.5; 577 | mesh.position.z += d - 0.5; 578 | // console.log('pos x 1', url, position, mesh); 579 | mesh.rotation.order = 'YXZ'; 580 | mesh.rotation.fromArray(rotation); 581 | mesh.rotation.y *= -1; 582 | mesh.rotation.z *= -1; 583 | mesh.scale.fromArray(scale); 584 | object.add(mesh); 585 | break; 586 | } 587 | case 'nft-image': { 588 | const {url, position, rotation, scale} = feature; 589 | const match = url.match(/https:\\/\\/opensea\\.io\\/assets\\/([^\\/]+)\\/([0-9]+)$/); 590 | if (match) { 591 | const contract = match[1]; 592 | const token = match[2]; 593 | const u = 'https://https-img-cryptovoxels-com.proxy.webaverse.com/node/opensea?contract=' + contract + '&token=' + token + '&force_update=0'; 594 | const res = await fetch(u); 595 | const j = await res.json(); 596 | const {image_url} = j; 597 | const geometry = imageGeometry; 598 | const material = _getTextureMaterialCached(image_url); 599 | const mesh = new THREE.Mesh(geometry, material); 600 | mesh.frustumCulled = false; 601 | mesh.position.fromArray(position); 602 | mesh.position.x *= -1; 603 | mesh.position.multiplyScalar(2); 604 | mesh.position.x -= w - 0.5; 605 | mesh.position.y += 0.5; 606 | mesh.position.z += d - 0.5; 607 | // console.log('pos x 2', url, position, mesh); 608 | mesh.rotation.order = 'YXZ'; 609 | mesh.rotation.fromArray(rotation); 610 | mesh.rotation.y *= -1; 611 | mesh.rotation.z *= -1; 612 | mesh.scale.fromArray(scale); 613 | object.add(mesh); 614 | } 615 | break; 616 | } 617 | case 'vox-model': { 618 | const {url, position, rotation, scale, flipX} = feature; 619 | // const u = new URL(url); 620 | // u.host = u.protocol.replace(/:/g, '-') + u.host.replace(/\\./g, '-') + '.proxy.webaverse.com'; 621 | // const model = await _loadVoxCached(u.href); 622 | const u = 'https://https-cdn-cryptovoxels-com.proxy.webaverse.com/node/vox?url=' + encodeURI(url); 623 | const model = await _loadVoxCached(u); 624 | // console.log('got u', u.href, model, {w, h, d, position, rotation, scale}); 625 | model.position.fromArray(position); 626 | model.position.x *= -1; 627 | model.position.multiplyScalar(2); 628 | model.position.x -= w - 0.5; 629 | model.position.y += 0.5; 630 | model.position.z += d - 0.5; 631 | model.rotation.order = 'YXZ'; 632 | model.rotation.fromArray(rotation); 633 | // model.rotation.x *= -1; 634 | model.rotation.y *= -1; 635 | model.rotation.z *= -1; 636 | model.scale.fromArray(scale).multiplyScalar(0.04); 637 | // model.scale.z *= -1; 638 | model.frustumCulled = false; 639 | object.add(model); 640 | break; 641 | } 642 | } 643 | } catch (err) { 644 | console.warn(err); 645 | } 646 | } 647 | } 648 | app.add(object); 649 | 650 | const _addPhysics = async () => { 651 | const mesh = object; 652 | 653 | mesh.updateMatrixWorld(); 654 | const physicsMesh = physics.convertMeshToPhysicsMesh(mesh); 655 | physicsMesh.position.copy(mesh.position); 656 | physicsMesh.quaternion.copy(mesh.quaternion); 657 | physicsMesh.scale.copy(mesh.scale); 658 | 659 | app.add(physicsMesh); 660 | const physicsId = physics.addGeometry(physicsMesh); 661 | app.remove(physicsMesh); 662 | physicsIds.push(physicsId); 663 | }; 664 | _addPhysics(); 665 | })(); 666 | 667 | useCleanup(() => { 668 | for (const physicsId of physicsIds) { 669 | physics.removeGeometry(physicsId); 670 | } 671 | }); 672 | 673 | // console.log('got app', app); 674 | 675 | return app; 676 | }; 677 | export const contentId = ${this.contentId}; 678 | export const name = ${this.name}; 679 | export const description = ${this.description}; 680 | export const type = 'js'; 681 | export const components = ${this.components}; -------------------------------------------------------------------------------- /contract_templates/loomlock.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, removeApp, useFrame, useLoaders, useCleanup, usePhysics, useLocalPlayer, useWeb3, useAbis, useInternals} = metaversefile; 4 | 5 | export default e => { 6 | const app = useApp(); 7 | const physics = usePhysics(); 8 | // const world = useWorld(); 9 | const {camera} = useInternals(); 10 | const web3 = useWeb3(); 11 | const {ERC721} = useAbis(); 12 | const ERC721LoomLock = JSON.parse(JSON.stringify(ERC721)); 13 | const tokenURIMethodAbi = ERC721LoomLock.find(m => m.name === 'tokenURI'); 14 | const preRevealTokenURIAbi = JSON.parse(JSON.stringify(tokenURIMethodAbi)); 15 | preRevealTokenURIAbi.name = 'preRevealTokenURI'; 16 | ERC721LoomLock.push(preRevealTokenURIAbi); 17 | 18 | const contractAddress = '${this.contractAddress}'; 19 | const tokenId = parseInt('${this.tokenId}', 10); 20 | // console.log('got token id', tokenId); 21 | 22 | const originalAppPosition = app.position.clone(); 23 | app.position.set(0, 0, 0); 24 | app.quaternion.set(0, 0, 0, 1); 25 | app.scale.set(1, 1, 1); 26 | 27 | const physicsIds = []; 28 | { 29 | const texture = new THREE.Texture(); 30 | const geometry = new THREE.PlaneBufferGeometry(1, 1, 100, 100); 31 | const uniforms = { 32 | map: { 33 | type: 't', 34 | value: texture, 35 | needsUpdate: true, 36 | }, 37 | uStartTime: { 38 | type: 'f', 39 | value: (Date.now()/1000) % 1, 40 | needsUpdate: true, 41 | }, 42 | uTime: { 43 | type: 'f', 44 | value: 0, 45 | needsUpdate: true, 46 | }, 47 | uHeadQuaternion: { 48 | type: 'q', 49 | value: new THREE.Quaternion(), 50 | needsUpdate: true, 51 | }, 52 | uCameraDirection: { 53 | type: 'v3', 54 | value: new THREE.Vector3(), 55 | needsUpdate: true, 56 | }, 57 | uCameraQuaternion: { 58 | type: 'q', 59 | value: new THREE.Quaternion(), 60 | needsUpdate: true, 61 | }, 62 | }; 63 | const vertexShader = \`\\ 64 | precision highp float; 65 | precision highp int; 66 | 67 | #define PI 3.1415926535897932384626433832795 68 | #define QUATERNION_IDENTITY vec4(0, 0, 0, 1) 69 | 70 | uniform float uStartTime; 71 | uniform float uTime; 72 | uniform vec4 uHeadQuaternion; 73 | uniform vec4 uCameraQuaternion; 74 | 75 | // varying vec3 vViewPosition; 76 | varying vec2 vUv; 77 | // varying vec3 vPosition; 78 | // varying vec3 vNormal; 79 | 80 | mat4 getRotationMatrix(vec4 quaternion) { 81 | // vec4 quaternion = uHeadQuaternion; 82 | float qw = quaternion.w; 83 | float qx = quaternion.x; 84 | float qy = quaternion.y; 85 | float qz = quaternion.z; 86 | 87 | float n = 1.0f/sqrt(qx*qx+qy*qy+qz*qz+qw*qw); 88 | qx *= n; 89 | qy *= n; 90 | qz *= n; 91 | qw *= n; 92 | 93 | return mat4( 94 | 1.0f - 2.0f*qy*qy - 2.0f*qz*qz, 2.0f*qx*qy - 2.0f*qz*qw, 2.0f*qx*qz + 2.0f*qy*qw, 0.0f, 95 | 2.0f*qx*qy + 2.0f*qz*qw, 1.0f - 2.0f*qx*qx - 2.0f*qz*qz, 2.0f*qy*qz - 2.0f*qx*qw, 0.0f, 96 | 2.0f*qx*qz - 2.0f*qy*qw, 2.0f*qy*qz + 2.0f*qx*qw, 1.0f - 2.0f*qx*qx - 2.0f*qy*qy, 0.0f, 97 | 0.0f, 0.0f, 0.0f, 1.0f); 98 | } 99 | vec4 q_slerp(vec4 a, vec4 b, float t) { 100 | // if either input is zero, return the other. 101 | if (length(a) == 0.0) { 102 | if (length(b) == 0.0) { 103 | return QUATERNION_IDENTITY; 104 | } 105 | return b; 106 | } else if (length(b) == 0.0) { 107 | return a; 108 | } 109 | 110 | float cosHalfAngle = a.w * b.w + dot(a.xyz, b.xyz); 111 | 112 | if (cosHalfAngle >= 1.0 || cosHalfAngle <= -1.0) { 113 | return a; 114 | } else if (cosHalfAngle < 0.0) { 115 | b.xyz = -b.xyz; 116 | b.w = -b.w; 117 | cosHalfAngle = -cosHalfAngle; 118 | } 119 | 120 | float blendA; 121 | float blendB; 122 | if (cosHalfAngle < 0.99) { 123 | // do proper slerp for big angles 124 | float halfAngle = acos(cosHalfAngle); 125 | float sinHalfAngle = sin(halfAngle); 126 | float oneOverSinHalfAngle = 1.0 / sinHalfAngle; 127 | blendA = sin(halfAngle * (1.0 - t)) * oneOverSinHalfAngle; 128 | blendB = sin(halfAngle * t) * oneOverSinHalfAngle; 129 | } else { 130 | // do lerp if angle is really small. 131 | blendA = 1.0 - t; 132 | blendB = t; 133 | } 134 | 135 | vec4 result = vec4(blendA * a.xyz + blendB * b.xyz, blendA * a.w + blendB * b.w); 136 | if (length(result) > 0.0) { 137 | return normalize(result); 138 | } 139 | return QUATERNION_IDENTITY; 140 | } 141 | vec3 applyQuaternion(vec3 v, vec4 q) { 142 | return v + 2.0*cross(cross(v, q.xyz ) + q.w*v, q.xyz); 143 | } 144 | 145 | void main() { 146 | float time = mod(uStartTime + uTime, 1.0); 147 | 148 | vec3 p = position; 149 | /* if (bar < 1.0) { 150 | float wobble = uDistance <= 0. ? sin(time * PI*10.)*0.02 : 0.; 151 | p.y *= (1.0 + wobble) * min(max(1. - uDistance/3., 0.), 1.0); 152 | } 153 | p.y += 0.01; */ 154 | const float headCutoff = 0.54; 155 | const float legsCutoff = 0.12; 156 | const float legsSplit = 0.5; 157 | const vec3 headOffset = vec3(0, 0.25, 0.); 158 | if (uv.y > headCutoff) { 159 | // float zOffset = (vec4(headOffset, 1.) * getRotationMatrix(q_slerp(uHeadQuaternion, vec4(0., 0., 0., 1.), (0.5 - abs(p.x)) * 2.))).z; 160 | float zOffset = (vec4(headOffset, 1.) * getRotationMatrix(uHeadQuaternion)).z; 161 | 162 | // p.z = sin(time * PI * 2.) * (uv.y - headCutoff); 163 | p -= headOffset; 164 | // p.xz *= 0.5; 165 | p = (vec4(p, 1.) * getRotationMatrix(uHeadQuaternion)).xyz; 166 | // p.xz *= 2.; 167 | p += headOffset; 168 | 169 | p.z += zOffset; 170 | } else if (uv.y < legsCutoff) { 171 | if (uv.x >= legsSplit) { 172 | p.z += sin(time * PI * 2.) * (legsCutoff - uv.y); 173 | } else { 174 | p.z += -sin(time * PI * 2.) * (legsCutoff - uv.y); 175 | } 176 | } 177 | vec4 mvPosition = modelViewMatrix * vec4(p, 1.0); 178 | // vPosition = mvPosition.xyz / mvPosition.w; 179 | gl_Position = projectionMatrix * mvPosition; 180 | vUv = uv; 181 | // vNormal = applyQuaternion(normal, uCameraQuaternion); 182 | } 183 | \`; 184 | const material = new THREE.ShaderMaterial({ 185 | uniforms, 186 | vertexShader, 187 | fragmentShader: \`\\ 188 | precision highp float; 189 | precision highp int; 190 | 191 | #define PI 3.1415926535897932384626433832795 192 | 193 | // uniform float uTime; 194 | uniform sampler2D map; 195 | uniform vec3 uCameraDirection; 196 | 197 | varying vec2 vUv; 198 | // varying vec3 vPosition; 199 | // varying vec3 vNormal; 200 | 201 | void main() { 202 | gl_FragColor = texture(map, vUv); 203 | if (gl_FragColor.a < 0.1) { 204 | discard; 205 | } 206 | } 207 | \`, 208 | transparent: true, 209 | side: THREE.BackSide, 210 | // polygonOffset: true, 211 | // polygonOffsetFactor: -1, 212 | // polygonOffsetUnits: 1, 213 | }); 214 | const imageMesh = new THREE.Mesh(geometry, material); 215 | imageMesh.position.copy(originalAppPosition); 216 | imageMesh.position.y = 0.5; 217 | imageMesh.quaternion.identity(); 218 | 219 | const materialBack = new THREE.ShaderMaterial({ 220 | uniforms, 221 | vertexShader, 222 | fragmentShader: \`\\ 223 | precision highp float; 224 | precision highp int; 225 | 226 | #define PI 3.1415926535897932384626433832795 227 | 228 | // uniform float uTime; 229 | uniform sampler2D map; 230 | uniform vec3 uCameraDirection; 231 | 232 | varying vec2 vUv; 233 | // varying vec3 vPosition; 234 | // varying vec3 vNormal; 235 | 236 | void main() { 237 | gl_FragColor = texture(map, vUv); 238 | if (gl_FragColor.a < 0.1) { 239 | discard; 240 | } 241 | gl_FragColor.rgb = vec3(0.); 242 | } 243 | \`, 244 | transparent: true, 245 | side: THREE.FrontSide, 246 | // polygonOffset: true, 247 | // polygonOffsetFactor: -1, 248 | // polygonOffsetUnits: 1, 249 | }); 250 | const imageMeshBack = new THREE.Mesh(geometry, materialBack); 251 | // imageMeshBack.rotation.order = 'YXZ'; 252 | // imageMeshBack.rotation.y = Math.PI; 253 | imageMesh.add(imageMeshBack); 254 | 255 | const _chooseAnimation = (timestamp, timeDiff) => { 256 | const player = useLocalPlayer(); 257 | 258 | const r = Math.random(); 259 | if (r < 0.5) { 260 | const velocity = new THREE.Vector3(0, 5, 0); 261 | // console.log('got time diff', timeDiff); 262 | return { 263 | type: 'jump', 264 | velocity, 265 | tick(timestamp, timeDiff) { 266 | imageMesh.position.add(velocity.clone().multiplyScalar(timeDiff/1000)); 267 | velocity.add(physics.getGravity().clone().multiplyScalar(timeDiff/1000)); 268 | if (imageMesh.position.y < 0.5) { 269 | imageMesh.position.y = 0.5; 270 | return true; 271 | } 272 | }, 273 | }; 274 | } else { 275 | const startPosition = imageMesh.position.clone(); 276 | const offset = new THREE.Vector3(-0.5 + Math.random(), 0, -0.5 + Math.random()).multiplyScalar(20); 277 | const offsetLength = offset.length(); 278 | if (offsetLength < 5) { 279 | offset.divideScalar(offsetLength).multiplyScalar(5); 280 | } 281 | const endPosition = startPosition.clone() 282 | .add(offset); 283 | const walkSpeed = 1/(0.1 + Math.random()); 284 | const startTime = timestamp; 285 | const endTime = startTime + (endPosition.distanceTo(startPosition) / walkSpeed) * 1000; 286 | const startQuaternion = imageMesh.quaternion.clone(); 287 | const endQuaternion = new THREE.Quaternion() 288 | .setFromRotationMatrix( 289 | new THREE.Matrix4().lookAt( 290 | startPosition, 291 | endPosition, 292 | new THREE.Vector3(0, 1, 0) 293 | ) 294 | ); 295 | const euler = new THREE.Euler().setFromQuaternion(endQuaternion, 'YXZ'); 296 | euler.x = 0; 297 | euler.z = 0; 298 | endQuaternion.setFromEuler(euler); 299 | 300 | let localDistanceTraveled = 0; 301 | return { 302 | type: 'walk', 303 | startPosition, 304 | endPosition, 305 | startQuaternion, 306 | endQuaternion, 307 | startTime, 308 | endTime, 309 | tick(timestamp, timeDiff) { 310 | const f = Math.min((timestamp - startTime) / (endTime - startTime), 1); 311 | const targetPosition = startPosition.clone().lerp(endPosition, f); 312 | if (f < 1) { 313 | const frameDistance = targetPosition.distanceTo(imageMesh.position); 314 | localDistanceTraveled += frameDistance; 315 | distanceTraveled += frameDistance; 316 | const targetQuaternion = startQuaternion.clone() 317 | .slerp( 318 | endQuaternion, 319 | Math.min(localDistanceTraveled, 1) 320 | ); 321 | 322 | imageMesh.position.copy(targetPosition); 323 | imageMesh.quaternion.copy(targetQuaternion); 324 | } else { 325 | imageMesh.position.copy(targetPosition); 326 | return true; 327 | } 328 | }, 329 | }; 330 | } 331 | }; 332 | // window.imageMesh = imageMesh; 333 | 334 | let animation = null; 335 | let distanceTraveled = 0; 336 | useFrame(({timestamp, timeDiff}) => { 337 | /* const _setToFloor = () => { 338 | if (!animation) { 339 | imageMesh.position.y = -app.position.y + 0.5; 340 | imageMesh.quaternion.copy(app.quaternion).invert(); 341 | } 342 | }; 343 | _setToFloor(); */ 344 | 345 | const _animate = () => { 346 | if (!animation) { 347 | animation = _chooseAnimation(timestamp, timeDiff); 348 | // console.log('new animation', animation); 349 | } 350 | const tickResult = animation.tick(timestamp, timeDiff); 351 | if (tickResult === true) { 352 | animation = null; 353 | } 354 | }; 355 | _animate(); 356 | 357 | const _setWalk = f => { 358 | // const f = (timestamp/1000) % 1; 359 | imageMesh.material.uniforms.uTime.value = f; 360 | imageMesh.material.uniforms.uTime.needsUpdate = true; 361 | }; 362 | _setWalk((distanceTraveled * 2) % 1); 363 | 364 | const _setLook = () => { 365 | const player = useLocalPlayer(); 366 | 367 | let lookQuaternion = new THREE.Quaternion().setFromRotationMatrix( 368 | new THREE.Matrix4().lookAt( 369 | imageMesh.position.clone() 370 | .add(new THREE.Vector3(0, 0.25, 0)), 371 | player.position, 372 | new THREE.Vector3(0, 1, 0) 373 | ) 374 | ); 375 | const lookEuler = new THREE.Euler().setFromQuaternion(lookQuaternion, 'YXZ'); 376 | // lookEuler.y += Math.PI; 377 | // lookEuler.x *= -1; 378 | // lookEuler.z = 0; 379 | lookQuaternion.setFromEuler(lookEuler); 380 | 381 | const angle = lookQuaternion.angleTo(imageMesh.quaternion); 382 | // console.log('got angle', angle); 383 | if (angle < Math.PI*0.4) { 384 | // nothing 385 | } else { 386 | lookQuaternion = imageMesh.quaternion.clone(); 387 | } 388 | 389 | imageMesh.material.uniforms.uHeadQuaternion.value.slerp(lookQuaternion.clone().premultiply(imageMesh.quaternion.clone().invert()), 0.1); // setFromAxisAngle(new THREE.Vector3(0, 1, 0), (-0.5 + f) * Math.PI); 390 | imageMesh.material.uniforms.uHeadQuaternion.needsUpdate = true; 391 | 392 | imageMesh.material.uniforms.uCameraDirection.value.set(0, 0, -1).applyQuaternion(camera.quaternion); 393 | imageMesh.material.uniforms.uCameraDirection.needsUpdate = true; 394 | 395 | imageMesh.material.uniforms.uCameraQuaternion.value.copy(camera.quaternion); 396 | imageMesh.material.uniforms.uCameraQuaternion.needsUpdate = true; 397 | }; 398 | _setLook(); 399 | }); 400 | 401 | (async () => { 402 | const contract = new web3.eth.Contract(ERC721LoomLock, contractAddress); 403 | 404 | const tokenURI = await contract.methods.preRevealTokenURI(tokenId).call(); 405 | const res = await fetch(tokenURI); 406 | const j = await res.json(); 407 | console.log('got loomlocknft j', j); 408 | 409 | const img = new Image(); 410 | await new Promise((accept, reject) => { 411 | img.onload = accept; 412 | img.onerror = reject; 413 | img.crossOrigin = 'Aynonymous'; 414 | img.src = j.image; 415 | }); 416 | 417 | const canvas = document.createElement('canvas'); 418 | canvas.width = img.naturalWidth; 419 | canvas.height = img.naturalHeight; 420 | const ctx = canvas.getContext('2d'); 421 | ctx.drawImage(img, 0, 0); 422 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 423 | const queue = [ 424 | [0, 0], 425 | [canvas.width-1, 0], 426 | [0, canvas.height-1], 427 | [canvas.width-1, canvas.height-1], 428 | ]; 429 | const seen = {}; 430 | const _getKey = (x, y) => x + ':' + y; 431 | while (queue.length > 0) { 432 | const [x, y] = queue.pop(); 433 | const k = _getKey(x, y); 434 | if (!seen[k]) { 435 | seen[k] = true; 436 | 437 | const startIndex = y*imageData.width*4 + x*4; 438 | const endIndex = startIndex + 4; 439 | const [r, g, b, a] = imageData.data.slice(startIndex, endIndex); 440 | if (r < 255/8 && g < 255/8 && b < 255/8) { 441 | // nothing 442 | } else { 443 | imageData.data[startIndex] = 0; 444 | imageData.data[startIndex+1] = 0; 445 | imageData.data[startIndex+2] = 0; 446 | imageData.data[startIndex+3] = 0; 447 | 448 | const _tryQueue = (x, y) => { 449 | if (x >= 0 && x < canvas.width && y >= 0 && y < canvas.height) { 450 | const k = _getKey(x, y); 451 | if (!seen[k]) { 452 | queue.push([x, y]); 453 | } 454 | } 455 | }; 456 | _tryQueue(x-1, y-1); 457 | _tryQueue(x, y-1); 458 | _tryQueue(x+1, y-1); 459 | 460 | _tryQueue(x-1, y); 461 | // _tryQueue(x, y); 462 | _tryQueue(x+1, y); 463 | 464 | _tryQueue(x-1, y+1); 465 | _tryQueue(x, y+1); 466 | _tryQueue(x+1, y+1); 467 | } 468 | } 469 | } 470 | ctx.putImageData(imageData, 0, 0); 471 | 472 | texture.image = canvas; 473 | texture.needsUpdate = true; 474 | imageMesh.material.uniforms.map.needsUpdate = true; 475 | })(); 476 | 477 | // imageMesh.position.set(0, 1.3, -0.2); 478 | app.add(imageMesh); 479 | 480 | const physicsId = physics.addBoxGeometry( 481 | imageMesh.position, 482 | imageMesh.quaternion, 483 | new THREE.Vector3(1/2, 1/2, 0.01), 484 | false 485 | ); 486 | physicsIds.push(physicsId); 487 | useFrame(() => { 488 | const p = imageMesh.position; 489 | const q = imageMesh.quaternion; 490 | const s = imageMesh.scale; 491 | physics.setPhysicsTransform(physicsId, p, q, s); 492 | }); 493 | } 494 | 495 | app.addEventListener('activate', e => { 496 | removeApp(app); 497 | app.destroy(); 498 | }); 499 | 500 | useCleanup(() => { 501 | for (const physicsId of physicsIds) { 502 | physics.removeGeometry(physicsId); 503 | } 504 | physicsIds.length = 0; 505 | }); 506 | 507 | return app; 508 | }; 509 | 510 | /* 511 | const npc = await world.addNpc(o.contentId, null, o.position, o.quaternion); 512 | 513 | const mesh = npc; 514 | const animations = mesh.getAnimations(); 515 | const component = mesh.getComponents()[componentIndex]; 516 | let {idleAnimation = ['idle'], aggroDistance, walkSpeed = 1} = component; 517 | if (idleAnimation) { 518 | if (!Array.isArray(idleAnimation)) { 519 | idleAnimation = [idleAnimation]; 520 | } 521 | } else { 522 | idleAnimation = []; 523 | } 524 | 525 | const idleAnimationClips = idleAnimation.map(name => animations.find(a => a.name === name)).filter(a => !!a); 526 | // console.log('got clips', npc, idleAnimationClips); 527 | const updateFns = []; 528 | if (idleAnimationClips.length > 0) { 529 | // hacks 530 | { 531 | mesh.position.y = 0; 532 | localEuler.setFromQuaternion(mesh.quaternion, 'YXZ'); 533 | localEuler.x = 0; 534 | localEuler.z = 0; 535 | mesh.quaternion.setFromEuler(localEuler); 536 | } 537 | 538 | const mixer = new THREE.AnimationMixer(mesh); 539 | const idleActions = idleAnimationClips.map(idleAnimationClip => mixer.clipAction(idleAnimationClip)); 540 | for (const idleAction of idleActions) { 541 | idleAction.play(); 542 | } 543 | 544 | updateFns.push(timeDiff => { 545 | const deltaSeconds = timeDiff / 1000; 546 | mixer.update(deltaSeconds); 547 | }); 548 | } 549 | 550 | let animation = null; 551 | updateFns.push(timeDiff => { 552 | const _updatePhysics = () => { 553 | const physicsIds = mesh.getPhysicsIds(); 554 | for (const physicsId of physicsIds) { 555 | physicsManager.setPhysicsTransform(physicsId, mesh.position, mesh.quaternion, mesh.scale); 556 | } 557 | }; 558 | 559 | if (animation) { 560 | mesh.position.add(localVector.copy(animation.velocity).multiplyScalar(timeDiff/1000)); 561 | animation.velocity.add(localVector.copy(physicsManager.getGravity()).multiplyScalar(timeDiff/1000)); 562 | if (mesh.position.y < 0) { 563 | animation = null; 564 | } 565 | 566 | _updatePhysics(); 567 | } else { 568 | const head = localPlayer.avatar.model.isVrm ? localPlayer.avatar.modelBones.Head : localPlayer.avatar.model; 569 | const position = head.getWorldPosition(localVector); 570 | position.y = 0; 571 | const distance = mesh.position.distanceTo(position); 572 | if (distance < aggroDistance) { 573 | const minDistance = 1; 574 | if (distance > minDistance) { 575 | const direction = position.clone().sub(mesh.position).normalize(); 576 | const maxMoveDistance = distance - minDistance; 577 | const moveDistance = Math.min(walkSpeed * timeDiff * 1000, maxMoveDistance); 578 | const moveDelta = direction.clone().multiplyScalar(moveDistance); 579 | mesh.position.add(moveDelta); 580 | 581 | const closestNpc = this.npcs.filter(n => n !== npc).sort((a, b) => { 582 | return a.position.distanceTo(npc.position) - b.position.distanceTo(npc.position); 583 | })[0]; 584 | const moveBufferDistance = 1; 585 | if (closestNpc && closestNpc.position.distanceTo(npc.position) >= (moveDistance + moveBufferDistance)) { 586 | mesh.quaternion.slerp(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), direction), 0.1); 587 | } else { 588 | mesh.position.sub(moveDelta); 589 | } 590 | 591 | _updatePhysics(); 592 | } 593 | } 594 | } 595 | }); 596 | npc.addEventListener('hit', e => { 597 | const euler = new THREE.Euler().setFromQuaternion(e.quaternion, 'YXZ'); 598 | euler.x = 0; 599 | euler.z = 0; 600 | const quaternion = new THREE.Quaternion().setFromEuler(euler); 601 | const hitSpeed = 1; 602 | animation = { 603 | velocity: new THREE.Vector3(0, 6, -5).applyQuaternion(quaternion).multiplyScalar(hitSpeed), 604 | }; 605 | }); 606 | npc.update = timeDiff => { 607 | for (const fn of updateFns) { 608 | fn(timeDiff); 609 | } 610 | }; 611 | this.npcs.push(npc); 612 | */ 613 | 614 | export const contentId = ${this.contentId}; 615 | export const name = ${this.name}; 616 | export const description = ${this.description}; 617 | export const type = 'js'; 618 | export const components = ${this.components}; -------------------------------------------------------------------------------- /contract_templates/moreloot.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, addTrackedApp, removeTrackedApp, useLocalPlayer, useActivate, useLoaders, useCleanup, usePhysics, useWeb3, useAbis} = metaversefile; 4 | 5 | const _capitalize = s => s.slice(0, 1).toUpperCase() + s.slice(1); 6 | const _capitalizeWords = s => { 7 | let words = s.split(/\\s/); 8 | words = words.map(_capitalize); 9 | return words.join(' '); 10 | }; 11 | const _underscoreWhitespace = s => s.replace(/\\s/g, '_'); 12 | const _getBaseName = s => { 13 | const match = s.match(/([^\\.\\/]*?)\\.[^\\.]*$/); 14 | return match ? match[1] : ''; 15 | }; 16 | const _normalizeName = name => { 17 | const weapons = [ 18 | "Warhammer", 19 | "Quarterstaff", 20 | "Maul", 21 | "Mace", 22 | "Club", 23 | "Katana", 24 | "Falchion", 25 | "Scimitar", 26 | "Long Sword", 27 | "Short Sword", 28 | "Ghost Wand", 29 | "Grave Wand", 30 | "Bone Wand", 31 | "Wand", 32 | "Grimoire", 33 | "Chronicle", 34 | "Tome", 35 | "Book" 36 | ]; 37 | const chestArmor = [ 38 | "Divine Robe", 39 | "Silk Robe", 40 | "Linen Robe", 41 | "Robe", 42 | "Shirt", 43 | "Demon Husk", 44 | "Dragonskin Armor", 45 | "Studded Leather Armor", 46 | "Hard Leather Armor", 47 | "Leather Armor", 48 | "Holy Chestplate", 49 | "Ornate Chestplate", 50 | "Plate Mail", 51 | "Chain Mail", 52 | "Ring Mail" 53 | ]; 54 | const headArmor = [ 55 | "Ancient Helm", 56 | "Ornate Helm", 57 | "Great Helm", 58 | "Full Helm", 59 | "Helm", 60 | "Demon Crown", 61 | "Dragon's Crown", 62 | "War Cap", 63 | "Leather Cap", 64 | "Cap", 65 | "Crown", 66 | "Divine Hood", 67 | "Silk Hood", 68 | "Linen Hood", 69 | "Hood" 70 | ]; 71 | const waistArmor = [ 72 | "Ornate Belt", 73 | "War Belt", 74 | "Plated Belt", 75 | "Mesh Belt", 76 | "Heavy Belt", 77 | "Demonhide Belt", 78 | "Dragonskin Belt", 79 | "Studded Leather Belt", 80 | "Hard Leather Belt", 81 | "Leather Belt", 82 | "Brightsilk Sash", 83 | "Silk Sash", 84 | "Wool Sash", 85 | "Linen Sash", 86 | "Sash" 87 | ]; 88 | const footArmor = [ 89 | "Holy Greaves", 90 | "Ornate Greaves", 91 | "Greaves", 92 | "Chain Boots", 93 | "Heavy Boots", 94 | "Demonhide Boots", 95 | "Dragonskin Boots", 96 | "Studded Leather Boots", 97 | "Hard Leather Boots", 98 | "Leather Boots", 99 | "Divine Slippers", 100 | "Silk Slippers", 101 | "Wool Shoes", 102 | "Linen Shoes", 103 | "Shoes" 104 | ]; 105 | const handArmor = [ 106 | "Holy Gauntlets", 107 | "Ornate Gauntlets", 108 | "Gauntlets", 109 | "Chain Gloves", 110 | "Heavy Gloves", 111 | "Demon's Hands", 112 | "Dragonskin Gloves", 113 | "Studded Leather Gloves", 114 | "Hard Leather Gloves", 115 | "Leather Gloves", 116 | "Divine Gloves", 117 | "Silk Gloves", 118 | "Wool Gloves", 119 | "Linen Gloves", 120 | "Gloves" 121 | ]; 122 | const necklaces = [ 123 | "Necklace", 124 | "Amulet", 125 | "Pendant" 126 | ]; 127 | const rings = [ 128 | "Gold Ring", 129 | "Silver Ring", 130 | "Bronze Ring", 131 | "Platinum Ring", 132 | "Titanium Ring" 133 | ]; 134 | const all = [weapons, chestArmor, headArmor, waistArmor, footArmor, handArmor, necklaces, rings].flat(); 135 | for (const n of all) { 136 | if (name.includes(n)) { 137 | return n; 138 | } 139 | } 140 | return null; 141 | }; 142 | 143 | const domParser = new DOMParser(); 144 | const xmlSerializer = new XMLSerializer(); 145 | export default e => { 146 | const app = useApp(); 147 | const physics = usePhysics(); 148 | const web3 = useWeb3(); 149 | const {ERC721} = useAbis(); 150 | 151 | const contractAddress = '${this.contractAddress}'; 152 | const tokenId = parseInt('${this.tokenId}', 10); 153 | console.log('got token id', tokenId); 154 | 155 | const apps = []; 156 | const physicsIds = []; 157 | e.waitUntil((async () => { 158 | const promises = []; 159 | 160 | const contract = new web3.eth.Contract(ERC721, contractAddress); 161 | // console.log('got contract', {ERC721, contractAddress, contract}); 162 | 163 | const tokenURI = await contract.methods.tokenURI(tokenId).call(); 164 | const res = await fetch(tokenURI); 165 | const j = await res.json(); 166 | // console.log('got moreloot j', j); 167 | 168 | promises.push((async () => { 169 | const texture = new THREE.Texture(); 170 | const geometry = new THREE.PlaneBufferGeometry(1, 1); 171 | const material = new THREE.MeshBasicMaterial({ 172 | map: texture, 173 | side: THREE.DoubleSide, 174 | }); 175 | const imageMesh = new THREE.Mesh(geometry, material); 176 | const img = await (async () => { 177 | const res = await fetch(j.image); 178 | const text = await res.text(); 179 | 180 | const doc = domParser.parseFromString(text, 'image/svg+xml'); 181 | const svg = doc.children[0]; 182 | svg.setAttribute('width', 1024); 183 | svg.setAttribute('height', 1024); 184 | const dataUrl = 'data:image/svg+xml;utf8,' + xmlSerializer.serializeToString(svg); 185 | 186 | const img = new Image(); 187 | await new Promise((accept, reject) => { 188 | img.onload = accept; 189 | img.onerror = reject; 190 | img.crossOrigin = 'Aynonymous'; 191 | img.src = dataUrl; 192 | }); 193 | return img; 194 | })(); 195 | texture.image = img; 196 | texture.needsUpdate = true; 197 | imageMesh.position.set(0, 1.3, -0.2); 198 | app.add(imageMesh); 199 | 200 | const physicsId = physics.addBoxGeometry( 201 | imageMesh.position, 202 | imageMesh.quaternion, 203 | new THREE.Vector3(1/2, 1/2, 0.01), 204 | false 205 | ); 206 | physicsIds.push(physicsId); 207 | })()); 208 | 209 | let spec; 210 | { 211 | const res2 = await fetch(j.image); 212 | const text = await res2.text(); 213 | const doc = domParser.parseFromString(text, 'image/svg+xml'); 214 | const svg = doc.children[0]; 215 | const elements = Array.from(doc.querySelectorAll('text')).map(e => e.innerHTML); 216 | // console.log('got doc', doc, Array.from(doc.children), elements); 217 | 218 | let index = 0; 219 | const slots = { 220 | weapon: elements[index++], 221 | chest: elements[index++], 222 | head: elements[index++], 223 | waist: elements[index++], 224 | foot: elements[index++], 225 | hand: elements[index++], 226 | neck: elements[index++], 227 | ring: elements[index++], 228 | }; 229 | const frontOffset = 0; 230 | const slotOuters = { 231 | weapon: { 232 | // boneAttachment: 'rightArm', 233 | position: new THREE.Vector3(-0.4, 1.1, 0.1 + frontOffset), 234 | quaternion: new THREE.Quaternion(), 235 | scale: new THREE.Vector3(1, 1, 1), 236 | }, 237 | chest: { 238 | position: new THREE.Vector3(0, 0.45, frontOffset), 239 | quaternion: new THREE.Quaternion(), 240 | scale: new THREE.Vector3(1, 1, 1), 241 | }, 242 | head: { 243 | position: new THREE.Vector3(0, 0.45, frontOffset), 244 | quaternion: new THREE.Quaternion(), 245 | scale: new THREE.Vector3(1, 1, 1), 246 | }, 247 | waist: { 248 | position: new THREE.Vector3(0, 0.3, frontOffset), 249 | quaternion: new THREE.Quaternion(), 250 | scale: new THREE.Vector3(1, 1, 1), 251 | }, 252 | foot: { 253 | position: new THREE.Vector3(0, 0.3, frontOffset), 254 | quaternion: new THREE.Quaternion(), 255 | scale: new THREE.Vector3(1, 1, 1), 256 | }, 257 | hand: { 258 | position: new THREE.Vector3(0, 0.4, frontOffset), 259 | quaternion: new THREE.Quaternion(), 260 | scale: new THREE.Vector3(1, 1, 1), 261 | }, 262 | neck: { 263 | position: new THREE.Vector3(0, 0.47, frontOffset), 264 | quaternion: new THREE.Quaternion(), 265 | scale: new THREE.Vector3(1, 1, 1), 266 | }, 267 | ring: { 268 | position: new THREE.Vector3(-0.6, 1.2, frontOffset), 269 | quaternion: new THREE.Quaternion(), 270 | scale: new THREE.Vector3(1, 1, 1), 271 | }, 272 | }; 273 | const slotInners = { 274 | weapon: { 275 | boneAttachment: 'leftHand', 276 | position: new THREE.Vector3(-0.07, -0.03, 0), 277 | quaternion: new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI/2), 278 | scale: new THREE.Vector3(1, 1, 1), 279 | }, 280 | chest: { 281 | skinnedMesh: true, 282 | /* boneAttachment: 'chest', 283 | position: new THREE.Vector3(0, -0.25, 0), 284 | quaternion: new THREE.Quaternion(), 285 | scale: new THREE.Vector3(1, 1, 1).multiplyScalar(1.5), */ 286 | }, 287 | head: { 288 | skinnedMesh: true, 289 | /* boneAttachment: 'head', 290 | position: new THREE.Vector3(0, 0, 0), 291 | quaternion: new THREE.Quaternion(), 292 | scale: new THREE.Vector3(1, 1, 1).multiplyScalar(1.5), */ 293 | }, 294 | waist: { 295 | skinnedMesh: true, 296 | /* boneAttachment: 'hips', 297 | position: new THREE.Vector3(0, 0, 0), 298 | quaternion: new THREE.Quaternion(), 299 | scale: new THREE.Vector3(1, 1, 1).multiplyScalar(1.3), */ 300 | }, 301 | foot: { 302 | skinnedMesh: true, 303 | /* boneAttachment: 'leftFoot', 304 | position: new THREE.Vector3(0, -0.13, 0.03), 305 | quaternion: new THREE.Quaternion(), 306 | scale: new THREE.Vector3(1, 1, 1).multiplyScalar(1.4), */ 307 | }, 308 | hand: { 309 | skinnedMesh: true, 310 | /* boneAttachment: 'leftHand', 311 | position: new THREE.Vector3(0, 0, 0), 312 | quaternion: new THREE.Quaternion(), 313 | scale: new THREE.Vector3(1, 1, 1), */ 314 | }, 315 | neck: { 316 | skinnedMesh: true, 317 | /* boneAttachment: 'neck', 318 | position: new THREE.Vector3(0, 0, 0), 319 | quaternion: new THREE.Quaternion(), 320 | scale: new THREE.Vector3(1, 1, 1), */ 321 | }, 322 | ring: { 323 | boneAttachment: 'leftRingFinger1', 324 | position: new THREE.Vector3(0, 0, 0), 325 | quaternion: new THREE.Quaternion(), 326 | scale: new THREE.Vector3(1, 1, 1), 327 | }, 328 | }; 329 | 330 | const slotNames = Object.keys(slots); 331 | const srcUrls = slotNames.map(k => { 332 | const v = _normalizeName(slots[k]); 333 | return 'https://webaverse.github.io/loot-assets/' + k + '/' + _underscoreWhitespace(_capitalizeWords(v)) + '/' + _underscoreWhitespace(v.toLowerCase()) + '.glb'; 334 | }); 335 | 336 | // console.log('loading', {slots, srcUrls}); 337 | 338 | const _makeComponents = (slotName, slotInner, srcUrl) => { 339 | const wearComponent = (() => { 340 | const {boneAttachment, skinnedMesh, position, quaternion, scale} = slotInner; 341 | let value; 342 | if (boneAttachment) { 343 | value = { 344 | boneAttachment, 345 | position: position.toArray(), 346 | quaternion: quaternion.toArray(), 347 | scale: scale.toArray(), 348 | }; 349 | } else if (skinnedMesh) { 350 | const baseName = _getBaseName(srcUrl); 351 | // console.log('got skinned mesh', _underscoreWhitespace(baseName.toLowerCase())); 352 | value = { 353 | skinnedMesh: _underscoreWhitespace(baseName.toLowerCase()), 354 | }; 355 | } 356 | return { 357 | key: 'wear', 358 | value, 359 | }; 360 | })(); 361 | 362 | const components = [ 363 | wearComponent, 364 | ]; 365 | if (slotName === 'weapon') { 366 | const useComponent = { 367 | key: "use", 368 | value: { 369 | animation: "combo", 370 | boneAttachment: "leftHand", 371 | position: [-0.07, -0.03, 0], 372 | quaternion: [0.7071067811865475, 0, 0, 0.7071067811865476], 373 | scale: [1, 1, 1] 374 | } 375 | }; 376 | components.push(useComponent); 377 | } 378 | return components; 379 | }; 380 | 381 | // const srcUrl = 'https://webaverse.github.io/loot-assets/chest/Ring_Mail/ring_mail.glb'; 382 | for (let i = 0; i < srcUrls.length; i++) { 383 | const srcUrl = srcUrls[i]; 384 | const slotName = slotNames[i]; 385 | const slotOuter = slotOuters[slotName]; 386 | const slotInner = slotInners[slotName]; 387 | 388 | /* if (Array.isArray(slotOuter.position)) { 389 | const ps = slotOuter.position.map((position, i) => { 390 | const quaternion = slotOuter.quaternion[i]; 391 | const scale = slotOuter.scale[i]; 392 | const components = _makeComponents({ 393 | boneAttachment: slotInner.boneAttachment[i], 394 | position: slotInner.position[i], 395 | quaternion: slotInner.quaternion[i], 396 | scale: slotInner.scale[i], 397 | }); 398 | const p = addTrackedApp( 399 | srcUrl, 400 | app.position.clone() 401 | .add(position.clone().applyQuaternion(app.quaternion)), 402 | app.quaternion 403 | .multiply(quaternion), 404 | scale, 405 | components 406 | ); 407 | p.then(app => { 408 | apps.push(app); 409 | }); 410 | return p; 411 | }); 412 | promises.push.apply(promises, ps); 413 | } else { */ 414 | const {position, quaternion, scale} = slotOuter; 415 | const components = _makeComponents(slotName, slotInner, srcUrl); 416 | 417 | // console.log('got loot components', srcUrl, components); 418 | const p = addTrackedApp( 419 | srcUrl, 420 | app.position.clone() 421 | .add(position.clone().applyQuaternion(app.quaternion)), 422 | app.quaternion 423 | .multiply(quaternion), 424 | scale, 425 | components 426 | ); 427 | promises.push(p); 428 | p.then(app => { 429 | apps.push(app); 430 | }); 431 | // } 432 | } 433 | await Promise.all(promises); 434 | } catch(err) { 435 | console.warn(err); 436 | throw err; 437 | } 438 | })()); 439 | 440 | useActivate(e => { 441 | for (const a of apps) { 442 | a.activate(); 443 | } 444 | removeApp(app); 445 | app.destroy(); 446 | }); 447 | 448 | useCleanup(() => { 449 | for (const physicsId of physicsIds) { 450 | physics.removeGeometry(physicsId); 451 | } 452 | physicsIds.length = 0; 453 | }); 454 | 455 | return app; 456 | }; 457 | 458 | export const contentId = ${this.contentId}; 459 | export const name = ${this.name}; 460 | export const description = ${this.description}; 461 | export const type = 'js'; 462 | export const components = ${this.components}; -------------------------------------------------------------------------------- /contracts/cryptovoxels.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'contract_templates', 'cryptovoxels.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | resolveId(source, importer) { 11 | return source; 12 | /* console.log('cv resolve id', {source, importer}); 13 | return '/@proxy/' + source; */ 14 | }, 15 | load(id) { 16 | // console.log('cv load id', {id}); 17 | id = id 18 | .replace(/^(eth?:\/(?!\/))/, '$1/'); 19 | 20 | const match = id.match(/^eth:\/\/(0x[0-9a-f]+)\/([0-9]+)$/i); 21 | if (match) { 22 | const contractAddress = match[1]; 23 | const tokenId = parseInt(match[2], 10); 24 | 25 | const { 26 | contentId, 27 | name, 28 | description, 29 | components, 30 | } = parseIdHash(id); 31 | 32 | const code = fillTemplate(templateString, { 33 | contractAddress, 34 | tokenId, 35 | contentId, 36 | name, 37 | description, 38 | components, 39 | }); 40 | // console.log('got glb id', id); 41 | return { 42 | code, 43 | map: null, 44 | }; 45 | } else { 46 | return null; 47 | } 48 | }, 49 | }; -------------------------------------------------------------------------------- /contracts/loomlock.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'contract_templates', 'loomlock.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | resolveId(source, importer) { 11 | return source; 12 | }, 13 | load(id) { 14 | console.log('loomlock load id', {id}); 15 | id = id 16 | .replace(/^(eth?:\/(?!\/))/, '$1/'); 17 | 18 | const match = id.match(/^eth:\/\/(0x[0-9a-f]+)\/([0-9]+)$/i); 19 | if (match) { 20 | const contractAddress = match[1]; 21 | const tokenId = parseInt(match[2], 10); 22 | 23 | const { 24 | contentId, 25 | name, 26 | description, 27 | components, 28 | } = parseIdHash(id); 29 | 30 | const code = fillTemplate(templateString, { 31 | contractAddress, 32 | tokenId, 33 | contentId, 34 | name, 35 | description, 36 | components, 37 | }); 38 | 39 | return { 40 | code, 41 | map: null, 42 | }; 43 | } else { 44 | return null; 45 | } 46 | }, 47 | }; -------------------------------------------------------------------------------- /contracts/moreloot.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'contract_templates', 'moreloot.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | resolveId(source, importer) { 11 | return source; 12 | }, 13 | load(id) { 14 | // console.log('moreloot load id', {id}); 15 | id = id 16 | .replace(/^(eth?:\/(?!\/))/, '$1/'); 17 | 18 | const match = id.match(/^eth:\/\/(0x[0-9a-f]+)\/([0-9]+)$/i); 19 | if (match) { 20 | const contractAddress = match[1]; 21 | const tokenId = parseInt(match[2], 10); 22 | 23 | const { 24 | contentId, 25 | name, 26 | description, 27 | components, 28 | } = parseIdHash(id); 29 | 30 | const code = fillTemplate(templateString, { 31 | contractAddress, 32 | tokenId, 33 | contentId, 34 | name, 35 | description, 36 | components, 37 | }); 38 | 39 | return { 40 | code, 41 | map: null, 42 | }; 43 | } else { 44 | return null; 45 | } 46 | }, 47 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | class Metaversefile extends EventTarget { 2 | constructor() { 3 | super(); 4 | } 5 | setApi(o) { 6 | for (const k in o) { 7 | Object.defineProperty(this, k, { 8 | value: o[k], 9 | }); 10 | } 11 | Object.freeze(this); 12 | } 13 | } 14 | const metaversefile = new Metaversefile(); 15 | 16 | export default metaversefile; 17 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const securityHeaders = [ 4 | { 5 | key: 'Access-Control-Allow-Origin', 6 | value: '*', 7 | }, 8 | { 9 | key: 'Cross-Origin-Opener-Policy', 10 | value: 'same-origin', 11 | }, 12 | { 13 | key: 'Cross-Origin-Embedder-Policy', 14 | value: 'require-corp', 15 | }, 16 | { 17 | key: 'Cross-Origin-Resource-Policy', 18 | value: 'cross-origin', 19 | }, 20 | ] 21 | 22 | const nextConfig = { 23 | reactStrictMode: true, 24 | swcMinify: true, 25 | trailingSlash: true, 26 | async headers() { 27 | return [ 28 | { 29 | // Apply these headers to all routes in your application. 30 | source: '/:path*', 31 | headers: securityHeaders, 32 | }, 33 | ] 34 | }, 35 | async rewrites() { 36 | return [ 37 | { 38 | source: '/:path*', 39 | destination: '/api/:path*', 40 | }, 41 | ]; 42 | }, 43 | }; 44 | export default nextConfig; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metaversefile", 3 | "version": "0.0.32", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/webaverse/metaversefile.git" 16 | }, 17 | "author": "Avaer Kazmer ", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/webaverse/metaversefile/issues" 21 | }, 22 | "homepage": "https://github.com/webaverse/metaversefile#readme", 23 | "dependencies": { 24 | "@babel/core": "^7.15.0", 25 | "@babel/preset-react": "^7.14.5", 26 | "data-urls": "^3.0.1", 27 | "esbuild": "^0.15.10", 28 | "mime-types": "^2.1.32", 29 | "node-fetch": "^2.6.1", 30 | "pako": "^2.0.4", 31 | "unix-path": "^0.2.0", 32 | "next": "12.3.1" 33 | }, 34 | "devDependencies": { 35 | "eslint": "8.25.0", 36 | "eslint-config-next": "12.3.1" 37 | } 38 | } -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | // import '../styles/globals.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /pages/api/[...slug].js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | import compile from '../../scripts/compile.js' 4 | 5 | export default async function handler(req, res) { 6 | // console.log('compile', req.url); 7 | 8 | try { 9 | let u = req.url.slice(1); 10 | if (u) { 11 | u = u.replace(/^([a-zA-Z0-9]+:\/(?!\/))/, '$1/'); 12 | const resultUint8Array = await compile(u); 13 | const resultBuffer = Buffer.from(resultUint8Array); 14 | res.setHeader('Content-Type', 'application/javascript'); 15 | res.end(resultBuffer); 16 | } else { 17 | res.status(404).send('not found'); 18 | } 19 | } catch(err) { 20 | console.warn(err); 21 | res.status(500).send(err.stack); 22 | } 23 | 24 | // res.status(200).json({ 25 | // url: req.url, 26 | // }); 27 | }; 28 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | // import Head from 'next/head' 2 | import {useEffect, useState} from 'react'; 3 | 4 | /* 5 | http://localhost:3000/https://webaverse.github.io/procgen-assets/avatars/male-procgen.vrm 6 | */ 7 | 8 | export default function Home() { 9 | const [baseUrl, setBaseUrl] = useState(''); 10 | useEffect(() => { 11 | setBaseUrl(globalThis.location.href); 12 | }, []); 13 | 14 | return ( 15 |
{baseUrl}URL_TO_COMPILE
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /plugins/rollup.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import url from 'url'; 3 | import mimeTypes from 'mime-types'; 4 | import {contractNames} from '../constants.js'; 5 | 6 | import cryptovoxels from '../contracts/cryptovoxels.js'; 7 | import moreloot from '../contracts/moreloot.js'; 8 | import loomlock from '../contracts/loomlock.js'; 9 | 10 | import jsx from '../types/jsx.js'; 11 | import metaversefile from '../types/metaversefile.js'; 12 | import glb from '../types/glb.js'; 13 | import vrm from '../types/vrm.js'; 14 | import vox from '../types/vox.js'; 15 | import image from '../types/image.js'; 16 | import gif from '../types/gif.js'; 17 | import glbb from '../types/glbb.js'; 18 | import gltj from '../types/gltj.js'; 19 | import html from '../types/html.js'; 20 | import scn from '../types/scn.js'; 21 | import light from '../types/light.js'; 22 | import text from '../types/text.js'; 23 | // import fog from '../types/fog.js'; 24 | // import background from '../types/background.js'; 25 | import rendersettings from '../types/rendersettings.js'; 26 | import spawnpoint from '../types/spawnpoint.js'; 27 | import wind from '../types/wind.js'; 28 | import lore from '../types/lore.js'; 29 | import quest from '../types/quest.js'; 30 | import npc from '../types/npc.js'; 31 | import mob from '../types/mob.js'; 32 | import react from '../types/react.js'; 33 | import group from '../types/group.js'; 34 | import vircadia from '../types/vircadia.js'; 35 | import directory from '../types/directory.js'; 36 | 37 | import upath from 'unix-path'; 38 | 39 | const contracts = { 40 | cryptovoxels, 41 | moreloot, 42 | loomlock, 43 | }; 44 | const loaders = { 45 | js: jsx, 46 | jsx, 47 | metaversefile, 48 | glb, 49 | vrm, 50 | vox, 51 | png: image, 52 | jpg: image, 53 | jpeg: image, 54 | svg: image, 55 | gif, 56 | glbb, 57 | gltj, 58 | html, 59 | scn, 60 | light, 61 | text, 62 | // fog, 63 | // background, 64 | rendersettings, 65 | spawnpoint, 66 | lore, 67 | quest, 68 | npc, 69 | mob, 70 | react, 71 | group, 72 | wind, 73 | vircadia, 74 | '': directory, 75 | }; 76 | 77 | const dataUrlRegex = /^data:([^;,]+)(?:;(charset=utf-8|base64))?,([\s\S]*)$/; 78 | const _getType = id => { 79 | id = id.replace(/^\/@proxy\//, ''); 80 | 81 | const o = url.parse(id, true); 82 | // console.log('get type', o, o.href.match(dataUrlRegex)); 83 | let match; 84 | if (o.href && (match = o.href.match(dataUrlRegex))) { 85 | let type = match[1] || ''; 86 | if (type === 'text/javascript') { 87 | type = 'application/javascript'; 88 | } 89 | let extension; 90 | let match2; 91 | if (match2 = type.match(/^application\/(light|text|rendersettings|spawnpoint|lore|quest|npc|mob|react|group|wind|vircadia)$/)) { 92 | extension = match2[1]; 93 | } else if (match2 = type.match(/^application\/(javascript)$/)) { 94 | extension = 'js'; 95 | } else { 96 | extension = mimeTypes.extension(type); 97 | } 98 | // console.log('got data extension', {type, extension}); 99 | return extension || ''; 100 | } else if (o.hash && (match = o.hash.match(/^#type=(.+)$/))) { 101 | return match[1] || ''; 102 | } else if (o.query && o.query.type) { 103 | return o.query.type; 104 | } else if (match = o.path.match(/\.([^\.\/]+)$/)) { 105 | return match[1].toLowerCase() || ''; 106 | } else { 107 | return ''; 108 | } 109 | }; 110 | 111 | const _resolvePathName = (pathName , source) => { 112 | /** 113 | * This check is specifically added because of windows 114 | * as windows is converting constantly all forward slashes into 115 | * backward slash 116 | */ 117 | if(process.platform === 'win32'){ 118 | pathName = pathName.replaceAll('\\','/').replaceAll('//','/'); 119 | pathName = path.resolve(upath.parse(pathName).dir, source); 120 | /** 121 | * Whenever path.resolve returns the result in windows it add the drive letter as well 122 | * Slice the drive letter (c:/, e:/, d:/ ) from the path and change backward slash 123 | * back to forward slash. 124 | */ 125 | pathName = pathName.slice(3).replaceAll('\\','/'); 126 | }else{ 127 | pathName = path.resolve(path.dirname(pathName), source); 128 | } 129 | return pathName; 130 | } 131 | 132 | const _resolveLoaderId = loaderId => { 133 | /** 134 | * This check is specifically added because of windows 135 | * as windows is converting constantly all forward slashes into 136 | * backward slash 137 | */ 138 | //console.log(loaderId); 139 | // const cwd = process.cwd(); 140 | if(process.platform === 'win32'){ 141 | //if(loaderId.startsWith(cwd) || loaderId.replaceAll('/','\\').startsWith(cwd)){ 142 | // loaderId = loaderId.slice(cwd.length); 143 | //}else if(loaderId.startsWith('http') || loaderId.startsWith('https')){ 144 | // loaderId = loaderId.replaceAll('\\','/'); 145 | //} 146 | loaderId = loaderId.replaceAll('\\','/'); 147 | 148 | // if(loaderId.startsWith('http') || loaderId.startsWith('https')){ 149 | // loaderId = loaderId.replaceAll('\\','/'); 150 | // } 151 | } 152 | return loaderId; 153 | } 154 | 155 | export default function metaversefilePlugin() { 156 | return { 157 | name: 'metaversefile', 158 | enforce: 'pre', 159 | async resolveId(source, importer) { 160 | // do not resolve node module subpaths 161 | if (/^((?:@[^\/]+\/)?[^\/:\.][^\/:]*)(\/[\s\S]*)$/.test(source)) { 162 | return null; 163 | } 164 | 165 | // scripts/compile.js: handle local compile case 166 | if (/^\.\//.test(source) && importer === '') { 167 | source = source.slice(1); 168 | } 169 | 170 | // console.log('rollup resolve id', {source, importer}); 171 | let replacedProxy = false; 172 | if (/^\/@proxy\//.test(source)) { 173 | source = source 174 | .replace(/^\/@proxy\//, '') 175 | .replace(/^(https?:\/(?!\/))/, '$1/'); 176 | replacedProxy = true; 177 | } 178 | if (/^ipfs:\/\//.test(source)) { 179 | source = source.replace(/^ipfs:\/\/(?:ipfs\/)?/, 'https://cloudflare-ipfs.com/ipfs/'); 180 | 181 | const o = url.parse(source, true); 182 | if (!o.query.type) { 183 | const res = await fetch(source, { 184 | method: 'HEAD', 185 | }); 186 | if (res.ok) { 187 | const contentType = res.headers.get('content-type'); 188 | const typeTag = mimeTypes.extension(contentType); 189 | if (typeTag) { 190 | source += `#type=${typeTag}`; 191 | } else { 192 | console.warn('unknown IPFS content type:', contentType); 193 | } 194 | // console.log('got content type', source, _getType(source)); 195 | } 196 | } 197 | } 198 | 199 | let match; 200 | if (match = source.match(/^eth:\/\/(0x[0-9a-f]+)\/([0-9]+)$/)) { 201 | const address = match[1]; 202 | const contractName = contractNames[address]; 203 | const contract = contracts[contractName]; 204 | const resolveId = contract?.resolveId; 205 | // console.log('check contract', resolveId); 206 | if (resolveId) { 207 | const source2 = await resolveId(source, importer); 208 | return source2; 209 | } 210 | } 211 | /* if (/^weba:\/\//.test(source)) { 212 | const {resolveId} = protocols.weba; 213 | const source2 = await resolveId(source, importer); 214 | return source2; 215 | } */ 216 | 217 | const type = _getType(source); 218 | const loader = loaders[type]; 219 | const resolveId = loader?.resolveId; 220 | if (resolveId) { 221 | const source2 = await resolveId(source, importer); 222 | // console.log('resolve rewrite', {type, source, source2}); 223 | if (source2 !== undefined) { 224 | return source2; 225 | } 226 | } 227 | if (replacedProxy) { 228 | // console.log('resolve replace', source); 229 | return source; 230 | } else { 231 | if (/^https?:\/\//.test(importer)) { 232 | const o = url.parse(importer); 233 | if (/\/$/.test(o.pathname)) { 234 | o.pathname += '.fakeFile'; 235 | } 236 | o.pathname = _resolvePathName(o.pathname,source); 237 | const s = '/@proxy/' + url.format(o); 238 | // console.log('resolve format', s); 239 | return s; 240 | } else if (/^https?:\/\//.test(source)) { 241 | return '/@proxy/' + source; 242 | } else { 243 | // console.log('resolve null'); 244 | return null; 245 | } 246 | } 247 | }, 248 | async load(id) { 249 | id = id 250 | // .replace(/^\/@proxy\//, '') 251 | .replace(/^(eth:\/(?!\/))/, '$1/') 252 | // .replace(/^(weba:\/(?!\/))/, '$1/'); 253 | 254 | let match; 255 | // console.log('contract load match', id.match(/^eth:\/\/(0x[0-9a-f]+)\/([0-9]+)$/)); 256 | if (match = id.match(/^eth:\/\/(0x[0-9a-f]+)\/([0-9]+)$/)) { 257 | const address = match[1]; 258 | const contractName = contractNames[address]; 259 | const contract = contracts[contractName]; 260 | const load = contract?.load; 261 | // console.log('load contract 1', load); 262 | if (load) { 263 | const src = await load(id); 264 | 265 | // console.log('load contract 2', src); 266 | if (src !== null && src !== undefined) { 267 | return src; 268 | } 269 | } 270 | } 271 | /* if (/^weba:\/\//.test(id)) { 272 | const {load} = protocols.weba; 273 | const src = await load(id); 274 | if (src !== null && src !== undefined) { 275 | return src; 276 | } 277 | } */ 278 | 279 | // console.log('load 2'); 280 | 281 | const type = _getType(id); 282 | const loader = loaders[type]; 283 | const load = loader?.load; 284 | 285 | if (load) { 286 | id = _resolveLoaderId(id); 287 | const src = await load(id); 288 | if (src !== null && src !== undefined) { 289 | return src; 290 | } 291 | } 292 | 293 | // console.log('load 2', {id, type, loader: !!loader, load: !!load}); 294 | 295 | if (/^https?:\/\//.test(id)) { 296 | const res = await fetch(id) 297 | const text = await res.text(); 298 | return text; 299 | } else if (match = id.match(dataUrlRegex)) { 300 | // console.log('load 3', match); 301 | // const type = match[1]; 302 | const encoding = match[2]; 303 | const src = match[3]; 304 | // console.log('load data url!!!', id, match); 305 | if (encoding === 'base64') { 306 | return atob(src); 307 | } else { 308 | return decodeURIComponent(src); 309 | } 310 | } else { 311 | return null; 312 | } 313 | }, 314 | async transform(src, id) { 315 | const type = _getType(id); 316 | const loader = loaders[type]; 317 | const transform = loader?.transform; 318 | if (transform) { 319 | return await transform(src, id); 320 | } 321 | return null; 322 | }, 323 | }; 324 | } -------------------------------------------------------------------------------- /scripts/compile.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | import metaversefilePlugin from '../plugins/rollup.js'; 3 | 4 | // const testModuleUrl = `./metaverse_modules/target-reticle/`; 5 | 6 | const metaversefilePluginInstance = metaversefilePlugin(); 7 | const metaversefilePluginProxy = { 8 | name: 'metaversefile', 9 | setup(build) { 10 | build.onResolve({filter: /^/}, async args => { 11 | const p = await metaversefilePluginInstance.resolveId(args.path, args.importer); 12 | return { 13 | path: p, 14 | namespace: 'metaversefile', 15 | }; 16 | }); 17 | build.onLoad({filter: /^/}, async args => { 18 | let c = await metaversefilePluginInstance.load(args.path); 19 | c = c.code; 20 | return { 21 | contents: c, 22 | }; 23 | }); 24 | }, 25 | }; 26 | 27 | async function compile(moduleUrl) { 28 | const o = await esbuild.build({ 29 | entryPoints: [ 30 | moduleUrl, 31 | ], 32 | // bundle: true, 33 | // outfile: 'out.js', 34 | plugins: [ 35 | metaversefilePluginProxy, 36 | ], 37 | // loader: { '.png': 'binary' }, 38 | write: false, 39 | outdir: 'out', 40 | }); 41 | if (o.outputFiles.length > 0) { 42 | return o.outputFiles[0].contents; 43 | } else if (o.errors.length > 0) { 44 | throw new Error(o.errors[0].text); 45 | } else { 46 | throw new Error('no output'); 47 | } 48 | } 49 | export default compile; 50 | 51 | // check if start script 52 | if (import.meta.url === `file://${process.argv[1]}`) { 53 | (async () => { 54 | const moduleUrl = process.argv[2]; 55 | if (!moduleUrl) { 56 | throw new Error('no module url specified'); 57 | } 58 | if (/^\.\.\//.test(moduleUrl)) { 59 | throw new Error('module url cannot be above current directory'); 60 | } 61 | 62 | const b = await compile(moduleUrl); 63 | console.log(b); 64 | })(); 65 | } -------------------------------------------------------------------------------- /type_templates/background.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useFrame, useCleanup, useInternals} = metaversefile; 4 | 5 | export default e => { 6 | const app = useApp(); 7 | const {scene} = useInternals(); 8 | 9 | const srcUrl = ${this.srcUrl}; 10 | 11 | let live = true; 12 | (async () => { 13 | const res = await fetch(srcUrl); 14 | if (!live) return; 15 | const j = await res.json(); 16 | if (!live) return; 17 | let {color} = j; 18 | if (Array.isArray(color) && color.length === 3 && color.every(n => typeof n === 'number')) { 19 | scene.background = new THREE.Color(color[0]/255, color[1]/255, color[2]/255); 20 | } 21 | })(); 22 | 23 | useCleanup(() => { 24 | live = false; 25 | scene.background = null; 26 | }); 27 | 28 | return app; 29 | }; 30 | export const contentId = ${this.contentId}; 31 | export const name = ${this.name}; 32 | export const description = ${this.description}; 33 | export const type = 'background'; 34 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/fog.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useInternals, useCleanup} = metaversefile; 4 | 5 | export default e => { 6 | const app = useApp(); 7 | const {rootScene} = useInternals(); 8 | 9 | const srcUrl = ${this.srcUrl}; 10 | const mode = app.getComponent('mode') ?? 'attached'; 11 | 12 | if (mode === 'attached') { 13 | let live = true; 14 | (async () => { 15 | const res = await fetch(srcUrl); 16 | if (!live) return; 17 | const fog = await res.json(); 18 | // console.log('got fog', fog); 19 | if (!live) return; 20 | /* if (fog.fogType === 'linear') { 21 | const {args = []} = fog; 22 | rootScene.fog = new THREE.Fog(new THREE.Color(args[0][0]/255, args[0][1]/255, args[0][2]/255).getHex(), args[1], args[2]); 23 | } else */if (fog.fogType === 'exp') { 24 | const {args = []} = fog; 25 | // console.log('got fog args', {fog, args}); 26 | rootScene.fog = new THREE.FogExp2(new THREE.Color(args[0][0]/255, args[0][1]/255, args[0][2]/255).getHex(), args[1]); 27 | } else { 28 | console.warn('unknown fog type:', fog.fogType); 29 | } 30 | })(); 31 | 32 | useCleanup(() => { 33 | live = false; 34 | rootScene.fog = null; 35 | }); 36 | } 37 | 38 | return app; 39 | }; 40 | export const contentId = ${this.contentId}; 41 | export const name = ${this.name}; 42 | export const description = ${this.description}; 43 | export const type = 'fog'; 44 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/gif.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import metaversefile from 'metaversefile'; 4 | const {useApp, useFrame, useCleanup, useLoaders, usePhysics} = metaversefile; 5 | 6 | const flipGeomeryUvs = geometry => { 7 | for (let i = 0; i < geometry.attributes.uv.array.length; i += 2) { 8 | const j = i + 1; 9 | geometry.attributes.uv.array[j] = 1 - geometry.attributes.uv.array[j]; 10 | } 11 | }; 12 | 13 | export default e => { 14 | const app = useApp(); 15 | const {gifLoader} = useLoaders(); 16 | const physics = usePhysics(); 17 | 18 | const srcUrl = ${this.srcUrl}; 19 | 20 | app.gif = null; 21 | 22 | const geometry = new THREE.PlaneBufferGeometry(1, 1); 23 | /* geometry.boundingBox = new THREE.Box3( 24 | new THREE.Vector3(-worldWidth/2, -worldHeight/2, -0.1), 25 | new THREE.Vector3(worldWidth/2, worldHeight/2, 0.1), 26 | ); */ 27 | flipGeomeryUvs(geometry); 28 | const colors = new Float32Array(geometry.attributes.position.array.length); 29 | colors.fill(1, 0, colors.length); 30 | geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); 31 | const material = new THREE.MeshBasicMaterial({ 32 | map: new THREE.Texture(), 33 | side: THREE.DoubleSide, 34 | vertexColors: true, 35 | transparent: true, 36 | alphaTest: 0.5, 37 | }); 38 | const model = new THREE.Mesh(geometry, material); 39 | model.frustumCulled = false; 40 | app.add(model); 41 | model.updateMatrixWorld(); 42 | 43 | let textures; 44 | let physicsIds = []; 45 | let staticPhysicsIds = []; 46 | e.waitUntil((async () => { 47 | const gifId = await gifLoader.createGif(srcUrl); 48 | const frames = await gifLoader.renderFrames(gifId); 49 | gifLoader.destroyGif(gifId); 50 | textures = frames.map(frame => { 51 | const t = new THREE.Texture(frame); 52 | t.anisotropy = 16; 53 | // t.encoding = THREE.sRGBEncoding; 54 | t.needsUpdate = true; 55 | return t; 56 | }); 57 | app.gif = frames; 58 | 59 | // set scale 60 | const frame = frames[0]; 61 | const {width, height} = frame; 62 | let worldWidth = width; 63 | let worldHeight = height; 64 | if (worldWidth >= worldHeight) { 65 | worldHeight /= worldWidth; 66 | worldWidth = 1; 67 | } 68 | if (worldHeight >= worldWidth) { 69 | worldWidth /= worldHeight; 70 | worldHeight = 1; 71 | } 72 | model.scale.set(worldWidth, worldHeight, 1); 73 | 74 | // add physics mesh 75 | const physicsId = physics.addBoxGeometry( 76 | app.position, 77 | app.quaternion, 78 | new THREE.Vector3(worldWidth/2, worldHeight/2, 0.01), 79 | false 80 | ); 81 | physicsIds.push(physicsId); 82 | staticPhysicsIds.push(physicsId); 83 | })()); 84 | 85 | useCleanup(() => { 86 | for (const physicsId of physicsIds) { 87 | physics.removeGeometry(physicsId); 88 | } 89 | physicsIds.length = 0; 90 | staticPhysicsIds.length = 0; 91 | }); 92 | useFrame(() => { 93 | if (textures) { 94 | const now = Date.now(); 95 | const f = (now % 2000) / 2000; 96 | const frameIndex = Math.floor(f * textures.length); 97 | material.map = textures[frameIndex]; 98 | } 99 | }); 100 | 101 | return app; 102 | }; 103 | export const contentId = ${this.contentId}; 104 | export const name = ${this.name}; 105 | export const description = ${this.description}; 106 | export const type = 'gif'; 107 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/glb.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import metaversefile from 'metaversefile'; 4 | const {useApp, useFrame, useCleanup, useLocalPlayer, usePhysics, useLoaders, useActivate, useAvatarInternal, useInternals} = metaversefile; 5 | 6 | // const wearableScale = 1; 7 | 8 | /* const localVector = new THREE.Vector3(); 9 | const localVector2 = new THREE.Vector3(); 10 | const localQuaternion = new THREE.Quaternion(); 11 | const localQuaternion2 = new THREE.Quaternion(); 12 | const localQuaternion3 = new THREE.Quaternion(); 13 | const localEuler = new THREE.Euler(); 14 | const localMatrix = new THREE.Matrix4(); */ 15 | 16 | // const z180Quaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI); 17 | 18 | export default e => { 19 | const app = useApp(); 20 | 21 | const physics = usePhysics(); 22 | const localPlayer = useLocalPlayer(); 23 | 24 | const srcUrl = ${this.srcUrl}; 25 | for (const {key, value} of components) { 26 | app.setComponent(key, value); 27 | } 28 | 29 | app.glb = null; 30 | const animationMixers = []; 31 | const uvScrolls = []; 32 | const physicsIds = []; 33 | app.physicsIds = physicsIds; 34 | 35 | // glb state 36 | let animations; 37 | 38 | // sit state 39 | let sitSpec = null; 40 | 41 | let activateCb = null; 42 | e.waitUntil((async () => { 43 | let o; 44 | try { 45 | o = await new Promise((accept, reject) => { 46 | const {gltfLoader} = useLoaders(); 47 | gltfLoader.load(srcUrl, accept, function onprogress() {}, reject); 48 | }); 49 | } catch(err) { 50 | console.warn(err); 51 | } 52 | // console.log('got o', o); 53 | if (o) { 54 | app.glb = o; 55 | const {parser} = o; 56 | animations = o.animations; 57 | // console.log('got animations', animations); 58 | o = o.scene; 59 | 60 | const _addAntialiasing = aaLevel => { 61 | o.traverse(o => { 62 | if (o.isMesh) { 63 | ['alphaMap', 'aoMap', 'bumpMap', 'displacementMap', 'emissiveMap', 'envMap', 'lightMap', 'map', 'metalnessMap', 'normalMap', 'roughnessMap'].forEach(mapType => { 64 | if (o.material[mapType]) { 65 | o.material[mapType].anisotropy = aaLevel; 66 | } 67 | }); 68 | if (o.material.transmission !== undefined) { 69 | o.material.transmission = 0; 70 | o.material.opacity = 0.25; 71 | } 72 | } 73 | }); 74 | }; 75 | _addAntialiasing(16); 76 | 77 | const _loadHubsComponents = () => { 78 | const _loadAnimations = () => { 79 | const animationEnabled = !!(app.getComponent('animation') ?? true); 80 | if (animationEnabled) { 81 | const idleAnimation = animations.find(a => a.name === 'idle'); 82 | const clips = idleAnimation ? [idleAnimation] : animations; 83 | for (const clip of clips) { 84 | const mixer = new THREE.AnimationMixer(o); 85 | 86 | const action = mixer.clipAction(clip); 87 | action.play(); 88 | 89 | animationMixers.push(mixer); 90 | } 91 | } 92 | }; 93 | if (!app.hasComponent('pet')) { 94 | _loadAnimations(); 95 | } 96 | 97 | const _loadLightmaps = () => { 98 | const _loadLightmap = async (parser, materialIndex) => { 99 | const lightmapDef = parser.json.materials[materialIndex].extensions.MOZ_lightmap; 100 | const [material, lightMap] = await Promise.all([ 101 | parser.getDependency('material', materialIndex), 102 | parser.getDependency('texture', lightmapDef.index) 103 | ]); 104 | material.lightMap = lightMap; 105 | material.lightMapIntensity = lightmapDef.intensity !== undefined ? lightmapDef.intensity : 1; 106 | material.needsUpdate = true; 107 | return lightMap; 108 | }; 109 | if (parser.json.materials) { 110 | for (let i = 0; i < parser.json.materials.length; i++) { 111 | const materialNode = parser.json.materials[i]; 112 | 113 | if (!materialNode.extensions) continue; 114 | 115 | if (materialNode.extensions.MOZ_lightmap) { 116 | _loadLightmap(parser, i); 117 | } 118 | } 119 | } 120 | }; 121 | _loadLightmaps(); 122 | 123 | const _loadUvScroll = o => { 124 | const textureToData = new Map(); 125 | const registeredTextures = []; 126 | o.traverse(o => { 127 | if (o.isMesh && o?.userData?.gltfExtensions?.MOZ_hubs_components?.['uv-scroll']) { 128 | const uvScrollSpec = o.userData.gltfExtensions.MOZ_hubs_components['uv-scroll']; 129 | const {increment, speed} = uvScrollSpec; 130 | 131 | const mesh = o; // this.el.getObject3D("mesh") || this.el.getObject3D("skinnedmesh"); 132 | const {material} = mesh; 133 | if (material) { 134 | const spec = { 135 | data: { 136 | increment, 137 | speed, 138 | }, 139 | }; 140 | 141 | // We store mesh here instead of the material directly because we end up swapping out the material in injectCustomShaderChunks. 142 | // We need material in the first place because of MobileStandardMaterial 143 | const instance = { component: spec, mesh }; 144 | 145 | spec.instance = instance; 146 | spec.map = material.map || material.emissiveMap; 147 | 148 | if (spec.map && !textureToData.has(spec.map)) { 149 | textureToData.set(spec.map, { 150 | offset: new THREE.Vector2(), 151 | instances: [instance] 152 | }); 153 | registeredTextures.push(spec.map); 154 | } else if (!spec.map) { 155 | console.warn("Ignoring uv-scroll added to mesh with no scrollable texture."); 156 | } else { 157 | console.warn( 158 | "Multiple uv-scroll instances added to objects sharing a texture, only the speed/increment from the first one will have any effect" 159 | ); 160 | textureToData.get(spec.map).instances.push(instance); 161 | } 162 | } 163 | let lastTimestamp = Date.now(); 164 | const update = now => { 165 | const dt = now - lastTimestamp; 166 | for (let i = 0; i < registeredTextures.length; i++) { 167 | const map = registeredTextures[i]; 168 | const { offset, instances } = textureToData.get(map); 169 | const { component } = instances[0]; 170 | 171 | offset.addScaledVector(component.data.speed, dt / 1000); 172 | 173 | offset.x = offset.x % 1.0; 174 | offset.y = offset.y % 1.0; 175 | 176 | const increment = component.data.increment; 177 | map.offset.x = increment.x ? offset.x - (offset.x % increment.x) : offset.x; 178 | map.offset.y = increment.y ? offset.y - (offset.y % increment.y) : offset.y; 179 | } 180 | lastTimestamp = now; 181 | }; 182 | uvScrolls.push({ 183 | update, 184 | }); 185 | } 186 | }); 187 | }; 188 | _loadUvScroll(o); 189 | }; 190 | _loadHubsComponents(); 191 | 192 | app.add(o); 193 | o.updateMatrixWorld(); 194 | 195 | const _addPhysics = async physicsComponent => { 196 | let physicsId; 197 | switch (physicsComponent.type) { 198 | case 'triangleMesh': { 199 | physicsId = physics.addGeometry(o); 200 | break; 201 | } 202 | case 'convexMesh': { 203 | physicsId = physics.addConvexGeometry(o); 204 | break; 205 | } 206 | default: { 207 | physicsId = null; 208 | break; 209 | } 210 | } 211 | if (physicsId !== null) { 212 | physicsIds.push(physicsId); 213 | } else { 214 | console.warn('glb unknown physics component', physicsComponent); 215 | } 216 | }; 217 | let physicsComponent = app.getComponent('physics'); 218 | if (physicsComponent) { 219 | if (physicsComponent === true) { 220 | physicsComponent = { 221 | type: 'triangleMesh', 222 | }; 223 | } 224 | _addPhysics(physicsComponent); 225 | } 226 | o.traverse(o => { 227 | if (o.isMesh) { 228 | o.frustumCulled = false; 229 | o.castShadow = true; 230 | o.receiveShadow = true; 231 | } 232 | }); 233 | 234 | activateCb = () => { 235 | if ( 236 | app.getComponent('sit') 237 | ) { 238 | app.wear(); 239 | } 240 | }; 241 | } 242 | })()); 243 | 244 | const _unwear = () => { 245 | if (sitSpec) { 246 | const sitAction = localPlayer.getAction('sit'); 247 | if (sitAction) { 248 | localPlayer.removeAction('sit'); 249 | } 250 | } 251 | }; 252 | app.addEventListener('wearupdate', e => { 253 | if (e.wear) { 254 | if (app.glb) { 255 | // const {animations} = app.glb; 256 | 257 | sitSpec = app.getComponent('sit'); 258 | if (sitSpec) { 259 | let rideMesh = null; 260 | app.glb.scene.traverse(o => { 261 | if (rideMesh === null && o.isSkinnedMesh) { 262 | rideMesh = o; 263 | } 264 | }); 265 | 266 | const {instanceId} = app; 267 | const localPlayer = useLocalPlayer(); 268 | 269 | const rideBone = sitSpec.sitBone ? rideMesh.skeleton.bones.find(bone => bone.name === sitSpec.sitBone) : null; 270 | const sitAction = { 271 | type: 'sit', 272 | time: 0, 273 | animation: sitSpec.subtype, 274 | controllingId: instanceId, 275 | controllingBone: rideBone, 276 | }; 277 | localPlayer.setControlAction(sitAction); 278 | } 279 | } 280 | } else { 281 | _unwear(); 282 | } 283 | }); 284 | 285 | useFrame(({timestamp, timeDiff}) => { 286 | const _updateAnimation = () => { 287 | const deltaSeconds = timeDiff / 1000; 288 | for (const mixer of animationMixers) { 289 | mixer.update(deltaSeconds); 290 | app.updateMatrixWorld(); 291 | } 292 | }; 293 | _updateAnimation(); 294 | 295 | const _updateUvScroll = () => { 296 | for (const uvScroll of uvScrolls) { 297 | uvScroll.update(timestamp); 298 | } 299 | }; 300 | _updateUvScroll(); 301 | }); 302 | 303 | useActivate(() => { 304 | activateCb && activateCb(); 305 | }); 306 | 307 | useCleanup(() => { 308 | for (const physicsId of physicsIds) { 309 | physics.removeGeometry(physicsId); 310 | } 311 | _unwear(); 312 | }); 313 | 314 | app.stop = () => { 315 | for (const mixer of animationMixers) { 316 | console.log('got mixer', mixer); 317 | mixer.stopAllAction(); 318 | } 319 | animationMixers.length = 0; 320 | }; 321 | 322 | return app; 323 | }; 324 | export const contentId = ${this.contentId}; 325 | export const name = ${this.name}; 326 | export const description = ${this.description}; 327 | export const type = 'glb'; 328 | export const components = ${this.components}; 329 | -------------------------------------------------------------------------------- /type_templates/glbb.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useFrame, useLoaders, usePhysics, useCleanup} = metaversefile; 4 | 5 | const size = 1024; 6 | const worldSize = 2; 7 | 8 | export default () => { 9 | const app = useApp(); 10 | const physics = usePhysics(); 11 | 12 | const o = new THREE.Object3D(); 13 | app.add(o); 14 | o.updateMatrixWorld(); 15 | 16 | let _update = null; 17 | 18 | const srcUrl = ${this.srcUrl}; 19 | (async () => { 20 | const {shadertoyLoader} = useLoaders(); 21 | const shadertoyRenderer = await shadertoyLoader.load(srcUrl, { 22 | size, 23 | worldSize, 24 | }); 25 | // await shadertoyRenderer.waitForLoad(); 26 | o.add(shadertoyRenderer.mesh); 27 | shadertoyRenderer.mesh.updateMatrixWorld(); 28 | _update = timeDiff => { 29 | shadertoyRenderer.update(timeDiff/1000); 30 | }; 31 | })(); 32 | 33 | let physicsIds = []; 34 | // let staticPhysicsIds = []; 35 | const _run = () => { 36 | const physicsId = physics.addBoxGeometry( 37 | app.position, 38 | app.quaternion, 39 | new THREE.Vector3(worldSize, worldSize, 0.01), 40 | false 41 | ); 42 | physicsIds.push(physicsId); 43 | // staticPhysicsIds.push(physicsId); 44 | }; 45 | _run(); 46 | 47 | useCleanup(() => { 48 | for (const physicsId of physicsIds) { 49 | physics.removeGeometry(physicsId); 50 | } 51 | physicsIds.length = 0; 52 | // staticPhysicsIds.length = 0; 53 | }); 54 | 55 | useFrame(({timeDiff}) => { 56 | _update && _update(timeDiff); 57 | }); 58 | 59 | return app; 60 | }; 61 | export const contentId = ${this.contentId}; 62 | export const name = ${this.name}; 63 | export const description = ${this.description}; 64 | export const type = 'glbb'; 65 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/gltj.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useFrame, useLoaders, useScene, usePhysics, useInternals, useJSON6Internal, useCleanup} = metaversefile; 4 | 5 | const {renderer} = useInternals(); 6 | const JSON6 = useJSON6Internal(); 7 | const geometry = new THREE.PlaneBufferGeometry(2, 2); 8 | 9 | export default e => { 10 | const app = useApp(); 11 | 12 | const srcUrl = ${this.srcUrl}; 13 | 14 | let _update = null; 15 | e.waitUntil((async () => { 16 | const res = await fetch(srcUrl); 17 | const s = await res.text(); 18 | const j = JSON6.parse(s); 19 | 20 | const material = new THREE.ShaderMaterial(j); 21 | 22 | const mesh = new THREE.Mesh(geometry, material); 23 | mesh.frustumCulled = false; 24 | app.add(mesh); 25 | mesh.updateMatrixWorld(); 26 | 27 | const uniforms = app.getComponent('uniforms'); 28 | for (const name in uniforms) { 29 | material.uniforms[name].value = uniforms[name]; 30 | } 31 | 32 | let now = 0; 33 | _update = (timestamp, timeDiff) => { 34 | if (material.uniforms.iTime) { 35 | material.uniforms.iTime.value = now/1000; 36 | material.uniforms.iTime.needsUpdate = true; 37 | } 38 | if (material.uniforms.iTimeS) { 39 | material.uniforms.iTimeS.value = timestamp/1000; 40 | material.uniforms.iTimeS.needsUpdate = true; 41 | } 42 | if (material.uniforms.iResolution) { 43 | if (!material.uniforms.iResolution.value) { 44 | material.uniforms.iResolution.value = new THREE.Vector3(); 45 | } 46 | const pixelRatio = renderer.getPixelRatio(); 47 | renderer.getSize(material.uniforms.iResolution.value) 48 | .multiplyScalar(pixelRatio); 49 | material.uniforms.iResolution.value.z = pixelRatio; 50 | material.uniforms.iResolution.needsUpdate = true; 51 | } 52 | 53 | now += timeDiff; 54 | }; 55 | })()); 56 | 57 | useFrame(({timestamp, timeDiff}) => { 58 | _update && _update(timestamp, timeDiff); 59 | }); 60 | 61 | return app; 62 | }; 63 | export const contentId = ${this.contentId}; 64 | export const name = ${this.name}; 65 | export const description = ${this.description}; 66 | export const type = 'gltj'; 67 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/group.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useFrame, useActivate, useWear, useUse, useCleanup, getNextInstanceId} = metaversefile; 4 | 5 | const localMatrix = new THREE.Matrix4(); 6 | 7 | function getObjectUrl(object) { 8 | let {start_url, type, content} = object; 9 | 10 | let u; 11 | if (start_url) { 12 | // make path relative to the .scn file 13 | u = /^\\.\\//.test(start_url) ? (new URL(import.meta.url).pathname.replace(/(\\/)[^\\/]*$/, '$1') + start_url.replace(/^\\.\\//, '')) : start_url; 14 | } else if (type && content) { 15 | if (typeof content === 'object') { 16 | content = JSON.stringify(content); 17 | } 18 | u = '/@proxy/data:' + type + ',' + encodeURI(content); 19 | } else { 20 | throw new Error('invalid scene object: ' + JSON.stringify(object)); 21 | } 22 | return u; 23 | } 24 | 25 | export default e => { 26 | const app = useApp(); 27 | 28 | const srcUrl = ${this.srcUrl}; 29 | 30 | const _updateSubAppMatrix = subApp => { 31 | // localMatrix.decompose(subApp.position, subApp.quaternion, subApp.scale); 32 | if (subApp === subApps[0]) { // group head 33 | subApp.updateMatrixWorld(); 34 | app.position.copy(subApp.position); 35 | app.quaternion.copy(subApp.quaternion); 36 | app.scale.copy(subApp.scale); 37 | app.matrix.copy(subApp.matrix); 38 | app.matrixWorld.copy(subApp.matrixWorld); 39 | } else { // group tail 40 | localMatrix.copy(subApp.offsetMatrix); 41 | if (subApps[0]) { 42 | localMatrix.premultiply(subApps[0].matrixWorld); 43 | } 44 | localMatrix.decompose(subApp.position, subApp.quaternion, subApp.scale); 45 | // /light/.test(subApp.name) && console.log('update subapp', subApp.position.toArray().join(', ')); 46 | subApp.updateMatrixWorld(); 47 | } 48 | }; 49 | 50 | let live = true; 51 | let subApps = []; 52 | e.waitUntil((async () => { 53 | const res = await fetch(srcUrl); 54 | const j = await res.json(); 55 | const {objects} = j; 56 | subApps = Array(objects.length); 57 | for (let i = 0; i < subApps.length; i++) { 58 | subApps[i] = null; 59 | } 60 | // console.log('group objects 1', objects); 61 | const promises = objects.map(async (object, i) => { 62 | if (live) { 63 | let {position = [0, 0, 0], quaternion = [0, 0, 0, 1], scale = [1, 1, 1], components = []} = object; 64 | position = new THREE.Vector3().fromArray(position); 65 | quaternion = new THREE.Quaternion().fromArray(quaternion); 66 | scale = new THREE.Vector3().fromArray(scale); 67 | 68 | let u2 = getObjectUrl(object); 69 | // console.log('add object', u2, {start_url, type, content}); 70 | 71 | // console.log('group objects 2', u2); 72 | 73 | if (/^https?:/.test(u2)) { 74 | u2 = '/@proxy/' + u2; 75 | } 76 | const m = await metaversefile.import(u2); 77 | // console.log('group objects 3', u2, m); 78 | const subApp = metaversefile.createApp({ 79 | name: u2, 80 | }); 81 | subApp.instanceId = getNextInstanceId(); 82 | if (i === 0) { // group head 83 | subApp.position.copy(app.position); 84 | subApp.quaternion.copy(app.quaternion); 85 | subApp.scale.copy(app.scale); 86 | } else { // group tail 87 | subApp.position.copy(position); 88 | subApp.quaternion.copy(quaternion); 89 | subApp.scale.copy(scale); 90 | } 91 | subApp.updateMatrixWorld(); 92 | subApp.contentId = u2; 93 | subApp.offsetMatrix = subApp.matrix.clone(); 94 | // console.log('group objects 3', subApp); 95 | subApp.setComponent('physics', true); 96 | for (const {key, value} of components) { 97 | subApp.setComponent(key, value); 98 | } 99 | subApps[i] = subApp; 100 | _updateSubAppMatrix(subApp); 101 | await subApp.addModule(m); 102 | // console.log('group objects 4', subApp); 103 | metaversefile.addApp(subApp); 104 | } 105 | }); 106 | await Promise.all(promises); 107 | })()); 108 | 109 | app.getPhysicsObjects = () => { 110 | const result = []; 111 | for (const subApp of subApps) { 112 | if (subApp) { 113 | result.push.apply(result, subApp.getPhysicsObjects()); 114 | } 115 | } 116 | return result; 117 | }; 118 | 119 | useFrame(() => { 120 | for (const subApp of subApps) { 121 | subApp && _updateSubAppMatrix(subApp); 122 | } 123 | }); 124 | 125 | useActivate(() => { 126 | for (const subApp of subApps) { 127 | subApp && subApp.activate(); 128 | } 129 | }); 130 | 131 | useWear(() => { 132 | for (const subApp of subApps) { 133 | subApp && subApp.wear(); 134 | } 135 | }); 136 | 137 | useUse(() => { 138 | for (const subApp of subApps) { 139 | subApp && subApp.use(); 140 | } 141 | }); 142 | 143 | useCleanup(() => { 144 | live = false; 145 | for (const subApp of subApps) { 146 | if (subApp) { 147 | metaversefile.removeApp(subApp); 148 | subApp.destroy(); 149 | } 150 | } 151 | }); 152 | 153 | return app; 154 | }; 155 | export const contentId = ${this.contentId}; 156 | export const name = ${this.name}; 157 | export const description = ${this.description}; 158 | export const type = 'group'; 159 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/html.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useFrame, useResize, useInternals, useLoaders, usePhysics, useCleanup} = metaversefile; 4 | 5 | const localVector = new THREE.Vector3(); 6 | const localVector2 = new THREE.Vector3(); 7 | const localVector3 = new THREE.Vector3(); 8 | const localQuaternion = new THREE.Quaternion(); 9 | const localMatrix = new THREE.Matrix4(); 10 | const localMatrix2 = new THREE.Matrix4(); 11 | 12 | class IFrameMesh extends THREE.Mesh { 13 | constructor({ 14 | // iframe, 15 | width, 16 | height, 17 | }) { 18 | const geometry = new THREE.PlaneBufferGeometry(width, height); 19 | const material = new THREE.MeshBasicMaterial({ 20 | color: 0xFFFFFF, 21 | side: THREE.DoubleSide, 22 | // colorWrite: false, 23 | // depthWrite: true, 24 | opacity: 0, 25 | transparent: true, 26 | blending: THREE.MultiplyBlending 27 | }); 28 | super(geometry, material); 29 | 30 | // this.iframe = iframe; 31 | } 32 | 33 | /* onBeforeRender(renderer, scene, camera, geometry, material, group) { 34 | super.onBeforeRender && super.onBeforeRender.apply(this, arguments); 35 | 36 | console.log('before render', this.iframe); 37 | } */ 38 | } 39 | 40 | export default e => { 41 | const app = useApp(); 42 | const physics = usePhysics(); 43 | 44 | const object = app; 45 | const { 46 | sceneHighPriority, 47 | camera, 48 | iframeContainer, 49 | } = useInternals(); 50 | const srcUrl = ${this.srcUrl}; 51 | const res = app.getComponent('resolution'); 52 | const width = res[0]; 53 | const height = res[1]; 54 | const scale = Math.min(1/width, 1/height); 55 | 56 | const _makeIframe = () => { 57 | const iframe = document.createElement('iframe'); 58 | iframe.setAttribute('width', width); 59 | iframe.setAttribute('height', height); 60 | iframe.style.width = width + 'px'; 61 | iframe.style.height = height + 'px'; 62 | // iframe.style.opacity = 0.75; 63 | iframe.style.background = 'white'; 64 | iframe.style.border = '0'; 65 | iframe.src = srcUrl; 66 | // window.iframe = iframe; 67 | iframe.style.width = width + 'px'; 68 | iframe.style.height = height + 'px'; 69 | iframe.style.visibility = 'hidden'; 70 | return iframe; 71 | }; 72 | let iframe = _makeIframe(); 73 | 74 | const iframeContainer2 = document.createElement('div'); 75 | iframeContainer2.style.cssText = 'position: absolute; left: 0; top: 0; bottom: 0; right: 0;'; 76 | iframeContainer.appendChild(iframeContainer2); 77 | iframeContainer2.appendChild(iframe); 78 | 79 | let fov = 0; 80 | const _updateSize = () => { 81 | fov = iframeContainer.getFov(); 82 | 83 | iframe.style.transform = 'translate(' + (window.innerWidth/2 - width/2) + 'px,' + (window.innerHeight/2 - height/2) + 'px) ' + getObjectCSSMatrix( 84 | localMatrix.compose( 85 | localVector.set(0, 0, 0), 86 | localQuaternion.set(0, 0, 0, 1), 87 | localVector2.setScalar(scale) 88 | ) 89 | ); 90 | iframe.style.pointerEvents = 'auto'; 91 | }; 92 | _updateSize(); 93 | 94 | const object2 = new IFrameMesh({ 95 | // iframe, 96 | width: width * scale, 97 | height: height * scale, 98 | }); 99 | object2.frustumCulled = false; 100 | 101 | function epsilon(value) { 102 | return value; 103 | } 104 | function getObjectCSSMatrix( matrix, cameraCSSMatrix ) { 105 | var elements = matrix.elements; 106 | var matrix3d = 'matrix3d(' + 107 | epsilon( elements[ 0 ] ) + ',' + 108 | epsilon( elements[ 1 ] ) + ',' + 109 | epsilon( elements[ 2 ] ) + ',' + 110 | epsilon( elements[ 3 ] ) + ',' + 111 | epsilon( - elements[ 4 ] ) + ',' + 112 | epsilon( - elements[ 5 ] ) + ',' + 113 | epsilon( - elements[ 6 ] ) + ',' + 114 | epsilon( - elements[ 7 ] ) + ',' + 115 | epsilon( elements[ 8 ] ) + ',' + 116 | epsilon( elements[ 9 ] ) + ',' + 117 | epsilon( elements[ 10 ] ) + ',' + 118 | epsilon( elements[ 11 ] ) + ',' + 119 | epsilon( elements[ 12 ] ) + ',' + 120 | epsilon( elements[ 13 ] ) + ',' + 121 | epsilon( elements[ 14 ] ) + ',' + 122 | epsilon( elements[ 15 ] ) + 123 | ')'; 124 | 125 | /* if ( isIE ) { 126 | 127 | return 'translate(-50%,-50%)' + 128 | 'translate(' + _widthHalf + 'px,' + _heightHalf + 'px)' + 129 | cameraCSSMatrix + 130 | matrix3d; 131 | 132 | } */ 133 | 134 | return matrix3d; 135 | } 136 | function getCameraCSSMatrix( matrix ) { 137 | const {elements} = matrix; 138 | return 'matrix3d(' + 139 | epsilon( elements[ 0 ] ) + ',' + 140 | epsilon( - elements[ 1 ] ) + ',' + 141 | epsilon( elements[ 2 ] ) + ',' + 142 | epsilon( elements[ 3 ] ) + ',' + 143 | epsilon( elements[ 4 ] ) + ',' + 144 | epsilon( - elements[ 5 ] ) + ',' + 145 | epsilon( elements[ 6 ] ) + ',' + 146 | epsilon( elements[ 7 ] ) + ',' + 147 | epsilon( elements[ 8 ] ) + ',' + 148 | epsilon( - elements[ 9 ] ) + ',' + 149 | epsilon( elements[ 10 ] ) + ',' + 150 | epsilon( elements[ 11 ] ) + ',' + 151 | epsilon( elements[ 12 ] ) + ',' + 152 | epsilon( - elements[ 13 ] ) + ',' + 153 | epsilon( elements[ 14 ] ) + ',' + 154 | epsilon( elements[ 15 ] ) + 155 | ')'; 156 | } 157 | object2.onBeforeRender = (renderer) => { 158 | const context = renderer.getContext(); 159 | context.disable(context.SAMPLE_ALPHA_TO_COVERAGE); 160 | }; 161 | object2.onAfterRender = (renderer) => { 162 | const context = renderer.getContext(); 163 | context.enable(context.SAMPLE_ALPHA_TO_COVERAGE); 164 | }; 165 | // const object = new THREE.Mesh(); 166 | // object.contentId = contentId; 167 | // object.frustumCulled = false; 168 | // object.isHtml = true; 169 | let physicsIds = []; 170 | let staticPhysicsIds = []; 171 | { 172 | object.matrixWorld.decompose(localVector, localQuaternion, localVector2); 173 | localVector2.multiply( 174 | localVector3.set( 175 | width * scale / 2, 176 | height * scale / 2, 177 | 0.001 178 | ) 179 | ); 180 | const physicsId = physics.addBoxGeometry( 181 | localVector, 182 | localQuaternion, 183 | localVector2, 184 | false 185 | ); 186 | physicsIds.push(physicsId); 187 | staticPhysicsIds.push(physicsId); 188 | 189 | iframe.addEventListener('load', e => { 190 | iframe.style.visibility = null; 191 | }, {once: true}); 192 | app.add( object2 ); 193 | } 194 | useCleanup(() => { 195 | for (const physicsId of physicsIds) { 196 | physics.removeGeometry(physicsId); 197 | } 198 | physicsIds.length = 0; 199 | staticPhysicsIds.length = 0; 200 | 201 | iframeContainer2.removeChild(iframe); 202 | iframeContainer2.parentElement.removeChild(iframeContainer2); 203 | }); 204 | object.getPhysicsIds = () => physicsIds; 205 | object.getStaticPhysicsIds = () => staticPhysicsIds; 206 | /* object.hit = () => { 207 | console.log('hit', object); // XXX 208 | return { 209 | hit: false, 210 | died: false, 211 | }; 212 | }; */ 213 | 214 | useFrame(e => { 215 | if (app.parent) { 216 | const cameraCSSMatrix = 217 | // 'translateZ(' + fov + 'px) ' + 218 | getCameraCSSMatrix( 219 | localMatrix.copy(camera.matrixWorldInverse) 220 | // .invert() 221 | .premultiply( 222 | localMatrix2.makeTranslation(0, 0, fov) 223 | ) 224 | .multiply( 225 | object.matrixWorld 226 | ) 227 | /* .premultiply( 228 | localMatrix2.makeScale(1/window.innerWidth, -1/window.innerHeight, 1) 229 | .invert() 230 | ) */ 231 | // .invert() 232 | ); 233 | iframeContainer2.style.transform = cameraCSSMatrix; 234 | iframeContainer2.style.visibility = null; 235 | } else { 236 | iframeContainer2.style.visibility = 'hidden'; 237 | } 238 | }); 239 | useResize(_updateSize); 240 | 241 | return object; 242 | }; 243 | export const contentId = ${this.contentId}; 244 | export const name = ${this.name}; 245 | export const description = ${this.description}; 246 | export const type = 'html'; 247 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/image.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import metaversefile from 'metaversefile'; 4 | const {useApp, useFrame, useCleanup, usePhysics} = metaversefile; 5 | 6 | /* const flipGeomeryUvs = geometry => { 7 | for (let i = 0; i < geometry.attributes.uv.array.length; i += 2) { 8 | const j = i + 1; 9 | geometry.attributes.uv.array[j] = 1 - geometry.attributes.uv.array[j]; 10 | } 11 | }; */ 12 | // console.log('got gif 0'); 13 | 14 | export default e => { 15 | const app = useApp(); 16 | // const {gifLoader} = useLoaders(); 17 | const physics = usePhysics(); 18 | 19 | app.image = null; 20 | 21 | const srcUrl = ${this.srcUrl}; 22 | // console.log('got gif 1'); 23 | 24 | const physicsIds = []; 25 | // const staticPhysicsIds = []; 26 | e.waitUntil((async () => { 27 | const img = await (async() => { 28 | for (let i = 0; i < 10; i++) { // hack: give it a few tries, sometimes images fail for some reason 29 | try { 30 | const img = await new Promise((accept, reject) => { 31 | const img = new Image(); 32 | img.onload = () => { 33 | accept(img); 34 | // startMonetization(instanceId, monetizationPointer, ownerAddress); 35 | // _cleanup(); 36 | }; 37 | img.onerror = err => { 38 | const err2 = new Error('failed to load image: ' + srcUrl + ': ' + err); 39 | reject(err2); 40 | // _cleanup(); 41 | } 42 | /* const _cleanup = () => { 43 | gcFiles && URL.revokeObjectURL(u); 44 | }; */ 45 | img.crossOrigin = 'Anonymous'; 46 | img.src = srcUrl; 47 | }); 48 | return img; 49 | } catch(err) { 50 | console.warn(err); 51 | } 52 | } 53 | throw new Error('failed to load image: ' + srcUrl); 54 | })(); 55 | app.image = img; 56 | let {width, height} = img; 57 | if (width >= height) { 58 | height /= width; 59 | width = 1; 60 | } 61 | if (height >= width) { 62 | width /= height; 63 | height = 1; 64 | } 65 | const geometry = new THREE.PlaneBufferGeometry(width, height); 66 | geometry.boundingBox = new THREE.Box3( 67 | new THREE.Vector3(-width/2, -height/2, -0.1), 68 | new THREE.Vector3(width/2, height/2, 0.1), 69 | ); 70 | const colors = new Float32Array(geometry.attributes.position.array.length); 71 | colors.fill(1, 0, colors.length); 72 | geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); 73 | 74 | const texture = new THREE.Texture(img); 75 | texture.anisotropy = 16; 76 | // texture.encoding = THREE.sRGBEncoding; 77 | texture.needsUpdate = true; 78 | const material = new THREE.MeshBasicMaterial({ 79 | map: texture, 80 | side: THREE.DoubleSide, 81 | vertexColors: true, 82 | transparent: true, 83 | alphaTest: 0.5, 84 | }); 85 | /* const material = meshComposer.material.clone(); 86 | material.uniforms.map.value = texture; 87 | material.uniforms.map.needsUpdate = true; */ 88 | 89 | const mesh = new THREE.Mesh(geometry, material); 90 | mesh.frustumCulled = false; 91 | // mesh.contentId = contentId; 92 | app.add(mesh); 93 | mesh.updateMatrixWorld(); 94 | 95 | const physicsId = physics.addBoxGeometry( 96 | app.position, 97 | app.quaternion, 98 | new THREE.Vector3(width/2, height/2, 0.01), 99 | false 100 | ); 101 | physicsIds.push(physicsId); 102 | // staticPhysicsIds.push(physicsId); 103 | })()); 104 | useCleanup(() => { 105 | for (const physicsId of physicsIds) { 106 | physics.removeGeometry(physicsId); 107 | } 108 | physicsIds.length = 0; 109 | // staticPhysicsIds.length = 0; 110 | }); 111 | 112 | return app; 113 | }; 114 | export const contentId = ${this.contentId}; 115 | export const name = ${this.name}; 116 | export const description = ${this.description}; 117 | export const type = 'image'; 118 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/light.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useFrame, useLocalPlayer, useCleanup, /*usePhysics, */ useWorld, useLightsManager} = metaversefile; 4 | 5 | const localVector = new THREE.Vector3(); 6 | const localVector2 = new THREE.Vector3(); 7 | 8 | export default e => { 9 | const app = useApp(); 10 | const lightsManager = useLightsManager(); 11 | 12 | const srcUrl = ${this.srcUrl}; 13 | 14 | const worldLights = app; 15 | app.light = null; 16 | 17 | let json = null; 18 | e.waitUntil((async () => { 19 | const res = await fetch(srcUrl); 20 | json = await res.json(); 21 | 22 | _render(); 23 | })()); 24 | 25 | const _render = () => { 26 | if (json !== null) { 27 | let {lightType, args, position, shadow} = json; 28 | const light = (() => { 29 | switch (lightType) { 30 | case 'ambient': { 31 | return new THREE.AmbientLight( 32 | new THREE.Color().fromArray(args[0]).multiplyScalar(1/255).getHex(), 33 | args[1] 34 | ); 35 | } 36 | case 'directional': { 37 | return new THREE.DirectionalLight( 38 | new THREE.Color().fromArray(args[0]).multiplyScalar(1/255).getHex(), 39 | args[1] 40 | ); 41 | } 42 | case 'point': { 43 | return new THREE.PointLight( 44 | new THREE.Color().fromArray(args[0]).multiplyScalar(1/255).getHex(), 45 | args[1], 46 | args[2], 47 | args[3] 48 | ); 49 | } 50 | case 'spot': { 51 | return new THREE.SpotLight( 52 | new THREE.Color().fromArray(args[0]).multiplyScalar(1/255).getHex(), 53 | args[1], 54 | args[2], 55 | args[3], 56 | args[4], 57 | args[5] 58 | ); 59 | } 60 | case 'rectArea': { 61 | return new THREE.RectAreaLight( 62 | new THREE.Color().fromArray(args[0]).multiplyScalar(1/255).getHex(), 63 | args[1], 64 | args[2], 65 | args[3] 66 | ); 67 | } 68 | case 'hemisphere': { 69 | return new THREE.HemisphereLight( 70 | new THREE.Color().fromArray(args[0]).multiplyScalar(1/255).getHex(), 71 | new THREE.Color().fromArray(args[1]).multiplyScalar(1/255).getHex(), 72 | args[2] 73 | ); 74 | } 75 | default: { 76 | return null; 77 | } 78 | } 79 | })(); 80 | if (light) { 81 | lightsManager.addLight(light, lightType, shadow, position); 82 | 83 | worldLights.add(light); 84 | if (light.target) { 85 | worldLights.add(light.target); 86 | } 87 | light.updateMatrixWorld(true); 88 | 89 | app.light = light; 90 | } else { 91 | console.warn('invalid light spec:', json); 92 | } 93 | } 94 | }; 95 | 96 | useFrame(() => { 97 | if (lightsManager.lights.length > 0) { 98 | for (const light of lightsManager.lights) { 99 | if (!light.lastAppMatrixWorld.equals(app.matrixWorld)) { 100 | light.position.copy(app.position); 101 | // light.quaternion.copy(app.quaternion); 102 | if (light.target) { 103 | light.quaternion.setFromRotationMatrix( 104 | new THREE.Matrix4().lookAt( 105 | light.position, 106 | light.target.position, 107 | localVector.set(0, 1, 0), 108 | ) 109 | ); 110 | } 111 | light.scale.copy(app.scale); 112 | light.matrix.copy(app.matrix); 113 | light.matrixWorld.copy(app.matrixWorld); 114 | light.lastAppMatrixWorld.copy(app.matrixWorld); 115 | light.updateMatrixWorld(); 116 | } 117 | } 118 | 119 | const localPlayer = useLocalPlayer(); 120 | for (const light of lightsManager.lights) { 121 | if (light.isDirectionalLight) { 122 | light.plane.setFromNormalAndCoplanarPoint(localVector.set(0, 0, -1).applyQuaternion(light.shadow.camera.quaternion), light.shadow.camera.position); 123 | const planeTarget = light.plane.projectPoint(localPlayer.position, localVector); 124 | // light.updateMatrixWorld(); 125 | const planeCenter = light.shadow.camera.position.clone(); 126 | 127 | const x = planeTarget.clone().sub(planeCenter) 128 | .dot(localVector2.set(1, 0, 0).applyQuaternion(light.shadow.camera.quaternion)); 129 | const y = planeTarget.clone().sub(planeCenter) 130 | .dot(localVector2.set(0, 1, 0).applyQuaternion(light.shadow.camera.quaternion)); 131 | 132 | light.shadow.camera.left = x + light.shadow.camera.initialLeft; 133 | light.shadow.camera.right = x + light.shadow.camera.initialRight; 134 | light.shadow.camera.top = y + light.shadow.camera.initialTop; 135 | light.shadow.camera.bottom = y + light.shadow.camera.initialBottom; 136 | light.shadow.camera.updateProjectionMatrix(); 137 | light.updateMatrixWorld(); 138 | } 139 | } 140 | } 141 | }); 142 | 143 | useCleanup(() => { 144 | for (const light of lightsManager.lights) { 145 | lightsManager.removeLight(light); 146 | } 147 | }); 148 | 149 | return app; 150 | }; 151 | export const contentId = ${this.contentId}; 152 | export const name = ${this.name}; 153 | export const description = ${this.description}; 154 | export const type = 'light'; 155 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/lore.js: -------------------------------------------------------------------------------- 1 | // import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useLoreAIScene, useCleanup} = metaversefile; 4 | 5 | export default e => { 6 | const app = useApp(); 7 | const loreAIScene = useLoreAIScene(); 8 | 9 | const srcUrl = ${this.srcUrl}; 10 | 11 | let live = true; 12 | let setting = null; 13 | (async () => { 14 | const res = await fetch(srcUrl); 15 | if (!live) return; 16 | let j = await res.json(); 17 | // console.log('got lore json', j); 18 | if (!live) return; 19 | if (Array.isArray(j)) { 20 | j = j.join('\\n'); 21 | } 22 | if (typeof j === 'string') { 23 | setting = loreAIScene.addSetting(j); 24 | } 25 | })(); 26 | 27 | useCleanup(() => { 28 | live = false; 29 | 30 | if (setting !== null) { 31 | loreAIScene.removeSetting(setting); 32 | setting = null; 33 | } 34 | }); 35 | 36 | return app; 37 | }; 38 | export const contentId = ${this.contentId}; 39 | export const name = ${this.name}; 40 | export const description = ${this.description}; 41 | export const type = 'lore'; 42 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/mob.js: -------------------------------------------------------------------------------- 1 | // import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useMobManager, useCleanup} = metaversefile; 4 | 5 | export default e => { 6 | const app = useApp(); 7 | const mobManager = useMobManager(); 8 | 9 | mobManager.addMobApp(app, srcUrl); 10 | 11 | useCleanup(() => { 12 | mobManager.removeMobApp(app); 13 | }); 14 | 15 | return app; 16 | }; 17 | export const contentId = ${this.contentId}; 18 | export const name = ${this.name}; 19 | export const description = ${this.description}; 20 | export const type = 'mob'; 21 | export const components = ${this.components}; 22 | export const srcUrl = ${this.srcUrl}; -------------------------------------------------------------------------------- /type_templates/npc.js: -------------------------------------------------------------------------------- 1 | // import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useNpcManager, useCleanup} = metaversefile; 4 | 5 | export default e => { 6 | const app = useApp(); 7 | const npcManager = useNpcManager(); 8 | 9 | const srcUrl = ${this.srcUrl}; 10 | 11 | e.waitUntil((async () => { 12 | await npcManager.addNpcApp(app, srcUrl); 13 | })()); 14 | 15 | useCleanup(() => { 16 | npcManager.removeNpcApp(app); 17 | }); 18 | 19 | return app; 20 | }; 21 | export const contentId = ${this.contentId}; 22 | export const name = ${this.name}; 23 | export const description = ${this.description}; 24 | export const type = 'npc'; 25 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/quest.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useDefaultModules, useQuests, useCleanup} = metaversefile; 4 | 5 | // const localVector = new THREE.Vector3(); 6 | // const localVector2 = new THREE.Vector3(); 7 | // const localQuaterion = new THREE.Quaternion(); 8 | 9 | export default e => { 10 | const app = useApp(); 11 | const defaultModules = useDefaultModules(); 12 | const questManager = useQuests(); 13 | 14 | const srcUrl = ${this.srcUrl}; 15 | 16 | const _makeAreaApp = ({ 17 | size, 18 | }) => { 19 | const areaApp = metaversefile.createApp({ 20 | module: defaultModules.modules.area, 21 | parent: app, 22 | components: { 23 | size, 24 | }, 25 | }); 26 | // console.log('create area app', areaApp); 27 | return areaApp; 28 | }; 29 | const _makeCameraPlaceholderApp = ({ 30 | position, 31 | quaternion, 32 | }) => { 33 | const cameraPlaceholderApp = metaversefile.createApp({ 34 | position: new THREE.Vector3().fromArray(position), 35 | quaternion: new THREE.Quaternion().fromArray(quaternion), 36 | module: defaultModules.modules.cameraPlaceholder, 37 | parent: app, 38 | }); 39 | return cameraPlaceholderApp; 40 | }; 41 | const _makePathApp = () => { 42 | const pathApp = metaversefile.createApp({ 43 | module: defaultModules.modules.path, 44 | parent: app, 45 | /* components: { 46 | line: [ 47 | [92.5, 0, -33], 48 | [19.5, -4, 59.5], 49 | ], 50 | }, */ 51 | }); 52 | return pathApp; 53 | }; 54 | 55 | const cameraPositionArray = app.getComponent('cameraPosition') ?? [0, 1.5, 0]; 56 | const cameraQuaternionArray = app.getComponent('cameraQuaternion') ?? [-0.3826834323650898, -0, -0, 0.9238795325112867]; 57 | const camera = new THREE.PerspectiveCamera(90, 1, 0.1, 3000); 58 | camera.position.fromArray(cameraPositionArray); 59 | camera.quaternion.fromArray(cameraQuaternionArray); 60 | app.add(camera); 61 | app.camera = camera; 62 | camera.updateMatrixWorld(); 63 | 64 | const size = app.getComponent('size') ?? [4, 2, 4]; 65 | const areaApp = _makeAreaApp({ 66 | size, 67 | }); 68 | const cameraPlaceholderApp = _makeCameraPlaceholderApp({ 69 | position: cameraPositionArray, 70 | quaternion: cameraQuaternionArray, 71 | }); 72 | const pathApp = _makePathApp(); 73 | 74 | app.json = null; 75 | const loadPromise = (async () => { 76 | const res = await fetch(srcUrl); 77 | app.json = await res.json(); 78 | })(); 79 | e.waitUntil(loadPromise); 80 | 81 | let quest = null; 82 | const _getPaused = () => app.getComponent('paused') ?? false; 83 | const _bindQuest = () => { 84 | quest = questManager.addQuest(app); 85 | }; 86 | const _unbindQuest = () => { 87 | questManager.removeQuest(quest); 88 | quest = null; 89 | }; 90 | const _checkPaused = async () => { 91 | await loadPromise; 92 | 93 | const paused = _getPaused(); 94 | if (!paused && quest === null) { 95 | _bindQuest(); 96 | } else if (paused && quest !== null) { 97 | _unbindQuest(); 98 | } 99 | }; 100 | _checkPaused(); 101 | 102 | app.addEventListener('componentsupdate', e => { 103 | if (e.keys.includes('paused')) { 104 | _checkPaused(); 105 | } 106 | }); 107 | 108 | useCleanup(() => { 109 | quest !== null && _unbindQuest(); 110 | }); 111 | 112 | return app; 113 | }; 114 | export const contentId = ${this.contentId}; 115 | export const name = ${this.name}; 116 | export const description = ${this.description}; 117 | export const type = 'quest'; 118 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/react.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useFrame, useDomRenderer, useInternals, useWear, useCleanup} = metaversefile; 4 | 5 | let baseUrl = import.meta.url.replace(/(\\/)[^\\/\\\\]*$/, '$1'); 6 | { 7 | const bu = new URL(baseUrl); 8 | const proxyPrefix ='/@proxy/'; 9 | if (bu.pathname.startsWith(proxyPrefix)) { 10 | baseUrl = bu.pathname.slice(proxyPrefix.length) + bu.search + bu.hash; 11 | } 12 | } 13 | 14 | export default e => { 15 | const app = useApp(); 16 | const {sceneLowerPriority} = useInternals(); 17 | const domRenderEngine = useDomRenderer(); 18 | 19 | let srcUrl = ${this.srcUrl}; 20 | 21 | let dom = null; 22 | // const transformMatrix = new THREE.Matrix4(); 23 | e.waitUntil((async () => { 24 | const res = await fetch(srcUrl); 25 | const json = await res.json(); 26 | let {/*position, quaternion, scale,*/ jsxUrl} = json; 27 | 28 | /* app.setComponent('wear', { 29 | boneAttachment: 'head', 30 | position, 31 | quaternion, 32 | scale, 33 | }); */ 34 | 35 | if (/^\\./.test(jsxUrl)) { 36 | jsxUrl = new URL(jsxUrl, baseUrl).href; 37 | } 38 | if (/^https?:\\/\\//.test(jsxUrl) && !jsxUrl.startsWith(location.origin)) { 39 | jsxUrl = '/@proxy/' + jsxUrl; 40 | } 41 | const m = await import(jsxUrl); 42 | 43 | dom = domRenderEngine.addDom({ 44 | render: () => m.default(), 45 | }); 46 | 47 | sceneLowerPriority.add(dom); 48 | dom.updateMatrixWorld(); 49 | })()); 50 | 51 | useFrame(() => { 52 | if (dom) { 53 | if (!wearing) { 54 | app.matrixWorld.decompose(dom.position, dom.quaternion, dom.scale); 55 | dom.updateMatrixWorld(); 56 | } else { 57 | dom.position.copy(app.position); 58 | dom.quaternion.copy(app.quaternion); 59 | dom.scale.copy(app.scale); 60 | dom.updateMatrixWorld(); 61 | } 62 | } 63 | }); 64 | 65 | let wearing = false; 66 | useWear(e => { 67 | wearing = e.wear; 68 | }); 69 | 70 | useCleanup(() => { 71 | if (dom) { 72 | dom.destroy(); 73 | } 74 | }); 75 | 76 | return app; 77 | }; 78 | export const contentId = ${this.contentId}; 79 | export const name = ${this.name}; 80 | export const description = ${this.description}; 81 | export const type = 'react'; 82 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/rendersettings.js: -------------------------------------------------------------------------------- 1 | // import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useInternals, useRenderSettings, usePostProcessing, useCleanup} = metaversefile; 4 | 5 | export default e => { 6 | const app = useApp(); 7 | const renderSettings = useRenderSettings(); 8 | 9 | const srcUrl = ${this.srcUrl}; 10 | 11 | let live = true; 12 | let json = null; 13 | let localRenderSettings = null; 14 | (async () => { 15 | const res = await fetch(srcUrl); 16 | if (!live) return; 17 | json = await res.json(); 18 | if (!live) return; 19 | localRenderSettings = renderSettings.makeRenderSettings(json); 20 | })(); 21 | 22 | useCleanup(() => { 23 | live = false; 24 | localRenderSettings = null; 25 | }); 26 | 27 | app.getRenderSettings = () => localRenderSettings; 28 | 29 | return app; 30 | }; 31 | export const contentId = ${this.contentId}; 32 | export const name = ${this.name}; 33 | export const description = ${this.description}; 34 | export const type = 'rendersettings'; 35 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/scn.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, createApp, createAppAsync, addTrackedApp, removeTrackedApp, useCleanup} = metaversefile; 4 | 5 | function typeContentToUrl(type, content) { 6 | if (typeof content === 'object') { 7 | content = JSON.stringify(content); 8 | } 9 | const dataUrlPrefix = 'data:' + type + ','; 10 | return '/@proxy/' + dataUrlPrefix + encodeURIComponent(content).replace(/\%/g, '%25')//.replace(/\\//g, '%2F'); 11 | } 12 | function getObjectUrl(object) { 13 | let {start_url, type, content} = object; 14 | 15 | let u; 16 | if (start_url) { 17 | // make path relative to the .scn file 18 | u = /^\\.\\//.test(start_url) ? (new URL(import.meta.url).pathname.replace(/(\\/)[^\\/]*$/, '$1') + start_url.replace(/^\\.\\//, '')) : start_url; 19 | } else if (type && content) { 20 | u = typeContentToUrl(type, content); 21 | } else { 22 | throw new Error('invalid scene object: ' + JSON.stringify(object)); 23 | } 24 | return u; 25 | } 26 | function mergeComponents(a, b) { 27 | const result = a.map(({ 28 | key, 29 | value, 30 | }) => ({ 31 | key, 32 | value, 33 | })); 34 | for (let i = 0; i < b.length; i++) { 35 | const bComponent = b[i]; 36 | const {key, value} = bComponent; 37 | let aComponent = result.find(c => c.key === key); 38 | if (!aComponent) { 39 | aComponent = { 40 | key, 41 | value, 42 | }; 43 | result.push(aComponent); 44 | } else { 45 | aComponent.value = value; 46 | } 47 | } 48 | return result; 49 | } 50 | 51 | export default e => { 52 | const app = useApp(); 53 | 54 | const srcUrl = ${this.srcUrl}; 55 | 56 | const mode = app.getComponent('mode') ?? 'attached'; 57 | const paused = app.getComponent('paused') ?? false; 58 | const objectComponents = app.getComponent('objectComponents') ?? []; 59 | // console.log('scn got mode', app.getComponent('mode'), 'attached'); 60 | const loadApp = (() => { 61 | switch (mode) { 62 | case 'detached': { 63 | return async (url, position, quaternion, scale, components) => { 64 | const components2 = {}; 65 | for (const {key, value} of components) { 66 | components2[key] = value; 67 | } 68 | for (const {key, value} of objectComponents) { 69 | components2[key] = value; 70 | } 71 | if (components2.mode === undefined) { 72 | components2.mode = 'detached'; 73 | } 74 | if (components2.paused === undefined) { 75 | components2.paused = paused; 76 | } 77 | 78 | const subApp = await createAppAsync({ 79 | start_url: url, 80 | position, 81 | quaternion, 82 | scale, 83 | parent: app, 84 | components: components2, 85 | }); 86 | // app.add(subApp); 87 | // console.log('scn app add subapp', app, subApp, subApp.parent); 88 | // subApp.updateMatrixWorld(); 89 | 90 | app.addEventListener('componentsupdate', e => { 91 | const {keys} = e; 92 | if (keys.includes('paused')) { 93 | const paused = app.getComponent('paused') ?? false; 94 | subApp.setComponent('paused', paused); 95 | } 96 | }); 97 | }; 98 | } 99 | case 'attached': { 100 | return async (url, position, quaternion, scale, components) => { 101 | components = mergeComponents(components, objectComponents); 102 | await addTrackedApp(url, position, quaternion, scale, components); 103 | }; 104 | } 105 | default: { 106 | throw new Error('unknown mode: ' + mode); 107 | } 108 | } 109 | })(); 110 | 111 | let live = true; 112 | e.waitUntil((async () => { 113 | const res = await fetch(srcUrl); 114 | const j = await res.json(); 115 | const {objects} = j; 116 | const buckets = {}; 117 | 118 | for (const object of objects) { 119 | const lp = object.loadPriority ?? 0; 120 | let a = buckets[lp]; 121 | if (!a) { 122 | a = []; 123 | buckets[lp] = a; 124 | } 125 | a.push(object); 126 | } 127 | 128 | const sKeys = Object.keys(buckets).sort((a, b) => a - b); 129 | 130 | for (let i=0; i { 133 | if (live) { 134 | let {position = [0, 0, 0], quaternion = [0, 0, 0, 1], scale = [1, 1, 1], components = []} = object; 135 | position = new THREE.Vector3().fromArray(position); 136 | quaternion = new THREE.Quaternion().fromArray(quaternion); 137 | scale = new THREE.Vector3().fromArray(scale); 138 | 139 | const url = getObjectUrl(object); 140 | await loadApp(url, position, quaternion, scale, components); 141 | } 142 | })); 143 | } 144 | })()); 145 | 146 | useCleanup(() => { 147 | live = false; 148 | }); 149 | 150 | app.hasSubApps = true; 151 | 152 | return true; 153 | }; 154 | export const contentId = ${this.contentId}; 155 | export const name = ${this.name}; 156 | export const description = ${this.description}; 157 | export const type = 'scn'; 158 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/spawnpoint.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useLocalPlayer} = metaversefile; 4 | 5 | const localEuler = new THREE.Euler(0, 0, 0, 'YXZ'); 6 | 7 | export default e => { 8 | const app = useApp(); 9 | 10 | const srcUrl = ${this.srcUrl}; 11 | const mode = app.getComponent('mode') ?? 'attached'; 12 | if (mode === 'attached') { 13 | (async () => { 14 | const res = await fetch(srcUrl); 15 | const j = await res.json(); 16 | if (j) { 17 | // const {camera} = useInternals(); 18 | 19 | const position = new THREE.Vector3(); 20 | const quaternion = new THREE.Quaternion(); 21 | const scale = new THREE.Vector3(1, 1, 1); 22 | if (j.position) { 23 | position.fromArray(j.position); 24 | } 25 | if (j.quaternion) { 26 | quaternion.fromArray(j.quaternion); 27 | localEuler.setFromQuaternion(quaternion, 'YXZ'); 28 | localEuler.x = 0; 29 | localEuler.z = 0; 30 | quaternion.setFromEuler(localEuler); 31 | } 32 | 33 | new THREE.Matrix4() 34 | .compose(position, quaternion, scale) 35 | .premultiply(app.matrixWorld) 36 | .decompose(position, quaternion, scale); 37 | 38 | const localPlayer = useLocalPlayer(); 39 | // if the avatar was not set, we'll need to set the spawn again when it is 40 | if (!localPlayer.avatar) { 41 | await new Promise((accept, reject) => { 42 | localPlayer.addEventListener('avatarchange', e => { 43 | const {avatar} = e; 44 | if (avatar) { 45 | accept(); 46 | } 47 | }); 48 | }); 49 | } 50 | localPlayer.setSpawnPoint(position, quaternion); 51 | } 52 | })(); 53 | } 54 | 55 | return app; 56 | }; 57 | export const contentId = ${this.contentId}; 58 | export const name = ${this.name}; 59 | export const description = ${this.description}; 60 | export const type = 'spawnpoint'; 61 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/text.js: -------------------------------------------------------------------------------- 1 | // import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, useText} = metaversefile; 4 | 5 | const Text = useText(); 6 | async function makeTextMesh( 7 | text = '', 8 | font = './assets/fonts/GeosansLight.ttf', 9 | fontSize = 1, 10 | anchorX = 'left', 11 | anchorY = 'middle', 12 | color = 0x000000, 13 | ) { 14 | const textMesh = new Text(); 15 | textMesh.text = text; 16 | textMesh.font = font; 17 | textMesh.fontSize = fontSize; 18 | textMesh.color = color; 19 | textMesh.anchorX = anchorX; 20 | textMesh.anchorY = anchorY; 21 | textMesh.frustumCulled = false; 22 | await new Promise(accept => { 23 | textMesh.sync(accept); 24 | }); 25 | return textMesh; 26 | } 27 | 28 | export default e => { 29 | const app = useApp(); 30 | 31 | const srcUrl = ${this.srcUrl}; 32 | 33 | app.text = null; 34 | 35 | e.waitUntil((async () => { 36 | const res = await fetch(srcUrl); 37 | const j = await res.json(); 38 | const {text, font, fontSize, anchorX, anchorY, color} = j; 39 | const textMesh = await makeTextMesh(text, font, fontSize, anchorX, anchorY, color); 40 | app.add(textMesh); 41 | app.text = textMesh; 42 | })()); 43 | 44 | return app; 45 | }; 46 | export const contentId = ${this.contentId}; 47 | export const name = ${this.name}; 48 | export const description = ${this.description}; 49 | export const type = 'text'; 50 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/vircadia.js: -------------------------------------------------------------------------------- 1 | import metaversefile from 'metaversefile'; 2 | const { useApp, useCleanup, useDomain } = metaversefile; 3 | 4 | export default e => { 5 | const app = useApp(); 6 | const domain = useDomain(); 7 | const srcUrl = ${ this.srcUrl }; 8 | let json = null; 9 | 10 | const mode = app.getComponent('mode') ?? 'attached'; 11 | if (mode === 'attached') { 12 | (async () => { 13 | if (domain) { 14 | const res = await fetch(srcUrl); 15 | json = await res.json(); 16 | if (json && json.domain) { 17 | if (!domain.hasURL()) { 18 | domain.connect(json.domain); 19 | } else { 20 | console.error('Tried to use more than one Vircadia domain in a scene.'); 21 | } 22 | } else { 23 | console.error("Invalid Vircadia domain spec:", json); 24 | } 25 | } else { 26 | console.error("Tried to use Vircadia domain in a non-domain scene."); 27 | } 28 | })(); 29 | } 30 | 31 | useCleanup(() => { 32 | // Don't need to call domain.disconnect() here because domain will have already been disconnected by 33 | // universe.disconnectDomain(). 34 | }); 35 | 36 | return app; 37 | }; 38 | export const contentId = ${ this.contentId }; 39 | export const name = ${ this.name }; 40 | export const description = ${ this.description }; 41 | export const type = 'domain'; 42 | export const components = ${ this.components }; 43 | -------------------------------------------------------------------------------- /type_templates/vox.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import metaversefile from 'metaversefile'; 4 | const {useApp, useFrame, useCleanup, useLoaders, usePhysics} = metaversefile; 5 | 6 | export default e => { 7 | const app = useApp(); 8 | const physics = usePhysics(); 9 | 10 | const srcUrl = ${this.srcUrl}; 11 | 12 | const root = app; 13 | 14 | const physicsIds = []; 15 | const staticPhysicsIds = []; 16 | e.waitUntil((async () => { 17 | let o; 18 | try { 19 | o = await new Promise((accept, reject) => { 20 | const {voxLoader} = useLoaders(); 21 | voxLoader.load(srcUrl, accept, function onprogress() {}, reject); 22 | }); 23 | // startMonetization(instanceId, monetizationPointer, ownerAddress); 24 | } catch(err) { 25 | console.warn(err); 26 | } 27 | 28 | root.add(o); 29 | 30 | const _addPhysics = async () => { 31 | const mesh = o; 32 | 33 | let physicsMesh = null; 34 | let physicsBuffer = null; 35 | /* if (physics_url) { 36 | if (files && _isResolvableUrl(physics_url)) { 37 | physics_url = files[_dotifyUrl(physics_url)]; 38 | } 39 | const res = await fetch(physics_url); 40 | const arrayBuffer = await res.arrayBuffer(); 41 | physicsBuffer = new Uint8Array(arrayBuffer); 42 | } else { */ 43 | mesh.updateMatrixWorld(); 44 | physicsMesh = physics.convertMeshToPhysicsMesh(mesh); 45 | physicsMesh.position.copy(mesh.position); 46 | physicsMesh.quaternion.copy(mesh.quaternion); 47 | physicsMesh.scale.copy(mesh.scale); 48 | 49 | // } 50 | 51 | if (physicsMesh) { 52 | root.add(physicsMesh); 53 | physicsMesh.updateMatrixWorld(); 54 | const physicsId = physics.addGeometry(physicsMesh); 55 | root.remove(physicsMesh); 56 | physicsIds.push(physicsId); 57 | staticPhysicsIds.push(physicsId); 58 | } 59 | if (physicsBuffer) { 60 | const physicsId = physics.addCookedGeometry(physicsBuffer, mesh.position, mesh.quaternion, mesh.scale); 61 | physicsIds.push(physicsId); 62 | staticPhysicsIds.push(physicsId); 63 | } 64 | /* for (const componentType of runComponentTypes) { 65 | const componentIndex = components.findIndex(component => component.type === componentType); 66 | if (componentIndex !== -1) { 67 | const component = components[componentIndex]; 68 | const componentHandler = componentHandlers[component.type]; 69 | const unloadFn = componentHandler.run(mesh, componentIndex); 70 | componentUnloadFns.push(unloadFn); 71 | } 72 | } */ 73 | }; 74 | if (app.getComponent('physics')) { 75 | _addPhysics(); 76 | } 77 | 78 | o.traverse(o => { 79 | if (o.isMesh) { 80 | o.frustumCulled = false; 81 | } 82 | }); 83 | })()); 84 | 85 | useCleanup(() => { 86 | for (const physicsId of physicsIds) { 87 | physics.removeGeometry(physicsId); 88 | } 89 | }); 90 | 91 | return root; 92 | }; 93 | export const contentId = ${this.contentId}; 94 | export const name = ${this.name}; 95 | export const description = ${this.description}; 96 | export const type = 'vox'; 97 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/vrm.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import metaversefile from 'metaversefile'; 3 | const {useApp, usePhysics, useAvatarRenderer, useCamera, useCleanup, useFrame, useActivate, useLocalPlayer} = metaversefile; 4 | 5 | const localVector = new THREE.Vector3(); 6 | const localVector2 = new THREE.Vector3(); 7 | const localQuaternion = new THREE.Quaternion(); 8 | const localMatrix = new THREE.Matrix4(); 9 | // const q180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI); 10 | 11 | const _fetchArrayBuffer = async srcUrl => { 12 | const res = await fetch(srcUrl); 13 | if (res.ok) { 14 | const arrayBuffer = await res.arrayBuffer(); 15 | return arrayBuffer; 16 | } else { 17 | throw new Error('failed to load: ' + res.status + ' ' + srcUrl); 18 | } 19 | }; 20 | 21 | export default e => { 22 | const app = useApp(); 23 | const camera = useCamera(); 24 | const physics = usePhysics(); 25 | 26 | const srcUrl = ${this.srcUrl}; 27 | const quality = app.getComponent('quality') ?? undefined; 28 | 29 | let avatarRenderer = null; 30 | let physicsIds = []; 31 | let activateCb = null; 32 | let frameCb = null; 33 | e.waitUntil((async () => { 34 | const arrayBuffer = await _fetchArrayBuffer(srcUrl); 35 | 36 | const AvatarRenderer = useAvatarRenderer(); 37 | avatarRenderer = new AvatarRenderer({ 38 | arrayBuffer, 39 | srcUrl, 40 | camera, 41 | quality, 42 | }); 43 | app.avatarRenderer = avatarRenderer; 44 | await avatarRenderer.waitForLoad(); 45 | app.add(avatarRenderer.scene); 46 | avatarRenderer.scene.updateMatrixWorld(); 47 | 48 | // globalThis.app = app; 49 | // globalThis.avatarRenderer = avatarRenderer; 50 | 51 | const _addPhysics = () => { 52 | const {height, width} = app.avatarRenderer.getAvatarSize(); 53 | 54 | const capsuleRadius = width / 2; 55 | const capsuleHalfHeight = height / 2; 56 | 57 | const halfAvatarCapsuleHeight = (height + width) / 2; // (full world height of the capsule) / 2 58 | 59 | localMatrix.compose( 60 | localVector.set(0, halfAvatarCapsuleHeight, 0), // start position 61 | localQuaternion.setFromAxisAngle(localVector2.set(0, 0, 1), Math.PI / 2), // rotate 90 degrees 62 | localVector2.set(capsuleRadius, halfAvatarCapsuleHeight, capsuleRadius) 63 | ) 64 | .premultiply(app.matrixWorld) 65 | .decompose(localVector, localQuaternion, localVector2); 66 | 67 | const physicsId = physics.addCapsuleGeometry( 68 | localVector, 69 | localQuaternion, 70 | capsuleRadius, 71 | capsuleHalfHeight, 72 | false 73 | ); 74 | physicsIds.push(physicsId); 75 | }; 76 | 77 | if (app.getComponent('physics')) { 78 | _addPhysics(); 79 | } 80 | 81 | // we don't want to have per-frame bone updates for unworn avatars 82 | const _disableSkeletonMatrixUpdates = () => { 83 | avatarRenderer.scene.traverse(o => { 84 | if (o.isBone) { 85 | o.matrixAutoUpdate = false; 86 | } 87 | }); 88 | }; 89 | _disableSkeletonMatrixUpdates(); 90 | 91 | // handle wearing 92 | activateCb = async () => { 93 | const localPlayer = useLocalPlayer(); 94 | localPlayer.setAvatarApp(app); 95 | }; 96 | 97 | frameCb = ({timestamp, timeDiff}) => { 98 | if (!avatarRenderer.isControlled) { 99 | avatarRenderer.scene.updateMatrixWorld(); 100 | avatarRenderer.update(timestamp, timeDiff); 101 | } 102 | }; 103 | })()); 104 | 105 | useActivate(() => { 106 | activateCb && activateCb(); 107 | }); 108 | 109 | useFrame((e) => { 110 | frameCb && frameCb(e); 111 | }); 112 | 113 | // controlled tracking 114 | const _setPhysicsEnabled = enabled => { 115 | if (enabled) { 116 | for (const physicsId of physicsIds) { 117 | physics.disableGeometry(physicsId); 118 | physics.disableGeometryQueries(physicsId); 119 | } 120 | } else { 121 | for (const physicsId of physicsIds) { 122 | physics.enableGeometry(physicsId); 123 | physics.enableGeometryQueries(physicsId); 124 | } 125 | } 126 | }; 127 | const _setControlled = controlled => { 128 | avatarRenderer && avatarRenderer.setControlled(controlled); 129 | _setPhysicsEnabled(controlled); 130 | }; 131 | _setControlled(!!app.getComponent('controlled')); 132 | app.addEventListener('componentupdate', e => { 133 | const {key, value} = e; 134 | if (key === 'controlled') { 135 | _setControlled(value); 136 | } 137 | }); 138 | 139 | // cleanup 140 | useCleanup(() => { 141 | for (const physicsId of physicsIds) { 142 | physics.removeGeometry(physicsId); 143 | } 144 | physicsIds.length = 0; 145 | }); 146 | 147 | return app; 148 | }; 149 | export const contentId = ${this.contentId}; 150 | export const name = ${this.name}; 151 | export const description = ${this.description}; 152 | export const type = 'vrm'; 153 | export const components = ${this.components}; -------------------------------------------------------------------------------- /type_templates/wind.js: -------------------------------------------------------------------------------- 1 | import metaversefile from 'metaversefile'; 2 | const {useApp, useCleanup, setWinds, removeWind} = metaversefile; 3 | 4 | export default e => { 5 | const app = useApp(); 6 | const srcUrl = ${this.srcUrl}; 7 | const mode = app.getComponent('mode') ?? 'attached'; 8 | let j = null; 9 | if (mode === 'attached') { 10 | (async () => { 11 | const res = await fetch(srcUrl); 12 | j = await res.json(); 13 | if (j) { 14 | setWinds(j); 15 | } 16 | })(); 17 | } 18 | 19 | useCleanup(() => { 20 | removeWind(j); 21 | }); 22 | 23 | return app; 24 | }; 25 | export const contentId = ${this.contentId}; 26 | export const name = ${this.name}; 27 | export const description = ${this.description}; 28 | export const type = 'wind'; 29 | export const components = ${this.components}; -------------------------------------------------------------------------------- /types/background.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const templateString = fs.readFileSync(path.join(__dirname, '..', 'type_templates', 'background.js'), 'utf8'); 6 | // const cwd = process.cwd(); 7 | 8 | module.exports = { 9 | load(id) { 10 | 11 | id = createRelativeFromAbsolutePath(id); 12 | 13 | const { 14 | contentId, 15 | name, 16 | description, 17 | components, 18 | } = parseIdHash(id); 19 | 20 | const code = fillTemplate(templateString, { 21 | srcUrl: JSON.stringify(id), 22 | contentId: JSON.stringify(contentId), 23 | name: JSON.stringify(name), 24 | description: JSON.stringify(description), 25 | components: JSON.stringify(components), 26 | }); 27 | 28 | return { 29 | code, 30 | map: null, 31 | }; 32 | }, 33 | }; -------------------------------------------------------------------------------- /types/directory.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import fetch from 'node-fetch'; 4 | import {fillTemplate, createRelativeFromAbsolutePath} from '../util.js'; 5 | import metaversefileLoader from './metaversefile.js'; 6 | 7 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 8 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'html.js')); 9 | 10 | const _resolveHtml = (id, importer) => { 11 | const code = fillTemplate(templateString, { 12 | srcUrl: JSON.stringify(id), 13 | }); 14 | return { 15 | code, 16 | map: null, 17 | }; 18 | }; 19 | 20 | export default { 21 | async resolveId(id, importer) { 22 | // const oldId = id; 23 | 24 | id = createRelativeFromAbsolutePath(id); 25 | 26 | // console.log('load directory', oldId, id, /^https?:\/\//.test(id), /\/$/.test(id)); 27 | if (/^https?:\/\//.test(id) && /\/$/.test(id)) { 28 | const metaversefilePath = id + '.metaversefile'; 29 | const res = await fetch(metaversefilePath, { 30 | method: 'HEAD', 31 | }); 32 | if (res.ok) { 33 | const metaversefileStartUrl = await metaversefileLoader.resolveId(metaversefilePath, id); 34 | // console.log('got metaversefile', {metaversefilePath, metaversefileStartUrl, id: id + '.fakeFile'}); 35 | return metaversefileStartUrl; 36 | } else { 37 | // console.log('got html', id, importer); 38 | 39 | const indexHtmlPath = id + 'index.html'; 40 | const res = await fetch(indexHtmlPath, { 41 | method: 'HEAD', 42 | }); 43 | if (res.ok) { 44 | return indexHtmlPath; 45 | } else { 46 | return null; 47 | } 48 | } 49 | } else if (/^\//.test(id)) { 50 | // console.log('got pre id 1', {id}); 51 | const cwd = process.cwd(); 52 | id = path.resolve(id); 53 | const idFullPath = path.join(cwd, id); 54 | const isDirectory = await new Promise((accept, reject) => { 55 | fs.lstat(idFullPath, (err, stats) => { 56 | accept(!err && stats.isDirectory()); 57 | }); 58 | }); 59 | if (isDirectory) { 60 | const metaversefilePath = path.join(id, '.metaversefile'); 61 | const metaversefileFullPath = path.join(cwd, metaversefilePath); 62 | const metaversefileExists = await new Promise((accept, reject) => { 63 | fs.lstat(metaversefileFullPath, (err, stats) => { 64 | accept(!err && stats.isFile()); 65 | }); 66 | }); 67 | // console.log('got pre id 2', {id, metaversefilePath, metaversefileFullPath, metaversefileExists}); 68 | if (metaversefileExists) { 69 | const fakeImporter = path.join(id, '.fakeFile'); 70 | const fakeId = path.join(path.dirname(fakeImporter), '.metaversefile'); 71 | // console.log('exists 1.1', {metaversefilePath, fakeId, fakeImporter}); 72 | const metaversefileStartUrl = await metaversefileLoader.resolveId(fakeId, fakeImporter); 73 | // console.log('exists 1.2', {metaversefilePath, metaversefileStartUrl}); 74 | // console.log('got metaversefile', {metaversefilePath, metaversefileStartUrl, id: id + '.fakeFile'}); 75 | return metaversefileStartUrl; 76 | } else { 77 | // console.log('exists 2'); 78 | 79 | const indexHtmlPath = path.join(id, 'index.html'); 80 | const indexHtmlFullPath = path.join(cwd, indexHtmlPath); 81 | const indexHtmlExists = await new Promise((accept, reject) => { 82 | fs.lstat(indexHtmlFullPath, (err, stats) => { 83 | accept(!err && stats.isFile()); 84 | }); 85 | }); 86 | 87 | if (indexHtmlExists) { 88 | // console.log('exists 3', {indexHtmlPath}); 89 | return indexHtmlPath; 90 | } else { 91 | // console.log('exists 4'); 92 | return null; 93 | } 94 | } 95 | } else { 96 | return null; 97 | } 98 | } else { 99 | return null; 100 | } 101 | }, 102 | load(id) { 103 | if (id === '/@react-refresh') { 104 | return null; 105 | } else { 106 | id = id.replace(/^\/@proxy\//, ''); 107 | return _resolveHtml(id); 108 | } 109 | } 110 | }; -------------------------------------------------------------------------------- /types/fog.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const templateString = fs.readFileSync(path.join(__dirname, '..', 'type_templates', 'fog.js'), 'utf8'); 6 | const cwd = process.cwd(); 7 | 8 | module.exports = { 9 | load(id) { 10 | 11 | id = createRelativeFromAbsolutePath(id); 12 | 13 | const { 14 | contentId, 15 | name, 16 | description, 17 | components, 18 | } = parseIdHash(id); 19 | 20 | const code = fillTemplate(templateString, { 21 | srcUrl: JSON.stringify(id), 22 | contentId: JSON.stringify(contentId), 23 | name: JSON.stringify(name), 24 | description: JSON.stringify(description), 25 | components: JSON.stringify(components), 26 | }); 27 | 28 | return { 29 | code, 30 | map: null, 31 | }; 32 | }, 33 | }; -------------------------------------------------------------------------------- /types/gif.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'gif.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | 12 | id = createRelativeFromAbsolutePath(id); 13 | 14 | const { 15 | contentId, 16 | name, 17 | description, 18 | components, 19 | } = parseIdHash(id); 20 | 21 | const code = fillTemplate(templateString, { 22 | srcUrl: JSON.stringify(id), 23 | contentId: JSON.stringify(contentId), 24 | name: JSON.stringify(name), 25 | description: JSON.stringify(description), 26 | components: JSON.stringify(components), 27 | }); 28 | 29 | return { 30 | code, 31 | map: null, 32 | }; 33 | }, 34 | }; -------------------------------------------------------------------------------- /types/glb.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'glb.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | /* function parseQuery(queryString) { 10 | const query = {}; 11 | const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&'); 12 | for (let i = 0; i < pairs.length; i++) { 13 | const pair = pairs[i].split('='); 14 | const k = decodeURIComponent(pair[0]); 15 | if (k) { 16 | const v = decodeURIComponent(pair[1] || ''); 17 | query[k] = v; 18 | } 19 | } 20 | return query; 21 | } */ 22 | 23 | export default { 24 | load(id) { 25 | id = createRelativeFromAbsolutePath(id); 26 | 27 | const { 28 | contentId, 29 | name, 30 | description, 31 | components, 32 | } = parseIdHash(id); 33 | 34 | // console.log('parse glb id', {id, contentId, name, description, components}); 35 | 36 | const code = fillTemplate(templateString, { 37 | srcUrl: JSON.stringify(id), 38 | contentId: JSON.stringify(contentId), 39 | name: JSON.stringify(name), 40 | description: JSON.stringify(description), 41 | components: JSON.stringify(components), 42 | }); 43 | return { 44 | code, 45 | map: null, 46 | }; 47 | }, 48 | }; -------------------------------------------------------------------------------- /types/glbb.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'glbb.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | 12 | id = createRelativeFromAbsolutePath(id); 13 | 14 | const { 15 | contentId, 16 | name, 17 | description, 18 | components, 19 | } = parseIdHash(id); 20 | 21 | const code = fillTemplate(templateString, { 22 | srcUrl: JSON.stringify(id), 23 | contentId: JSON.stringify(contentId), 24 | name: JSON.stringify(name), 25 | description: JSON.stringify(description), 26 | components: JSON.stringify(components), 27 | }); 28 | 29 | return { 30 | code, 31 | map: null, 32 | }; 33 | }, 34 | }; -------------------------------------------------------------------------------- /types/gltj.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'gltj.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | 12 | id = createRelativeFromAbsolutePath(id); 13 | 14 | const { 15 | contentId, 16 | name, 17 | description, 18 | components, 19 | } = parseIdHash(id); 20 | 21 | const code = fillTemplate(templateString, { 22 | srcUrl: JSON.stringify(id), 23 | contentId: JSON.stringify(contentId), 24 | name: JSON.stringify(name), 25 | description: JSON.stringify(description), 26 | components: JSON.stringify(components), 27 | }); 28 | 29 | return { 30 | code, 31 | map: null, 32 | }; 33 | }, 34 | }; -------------------------------------------------------------------------------- /types/group.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'group.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | id = createRelativeFromAbsolutePath(id); 12 | 13 | const { 14 | contentId, 15 | name, 16 | description, 17 | components, 18 | } = parseIdHash(id); 19 | 20 | const code = fillTemplate(templateString, { 21 | srcUrl: JSON.stringify(id), 22 | contentId: JSON.stringify(contentId), 23 | name: JSON.stringify(name), 24 | description: JSON.stringify(description), 25 | components: JSON.stringify(components), 26 | }); 27 | 28 | return { 29 | code, 30 | map: null, 31 | }; 32 | }, 33 | }; -------------------------------------------------------------------------------- /types/html.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'html.js'), 'utf8'); 7 | 8 | export default { 9 | load(id) { 10 | id = createRelativeFromAbsolutePath(id); 11 | 12 | const { 13 | contentId, 14 | name, 15 | description, 16 | components, 17 | } = parseIdHash(id); 18 | 19 | const code = fillTemplate(templateString, { 20 | srcUrl: JSON.stringify(id), 21 | contentId: JSON.stringify(contentId), 22 | name: JSON.stringify(name), 23 | description: JSON.stringify(description), 24 | components: JSON.stringify(components), 25 | }); 26 | 27 | return { 28 | code, 29 | map: null, 30 | }; 31 | }, 32 | }; -------------------------------------------------------------------------------- /types/image.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'image.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | 12 | id = createRelativeFromAbsolutePath(id); 13 | 14 | const { 15 | contentId, 16 | name, 17 | description, 18 | components, 19 | } = parseIdHash(id); 20 | 21 | const code = fillTemplate(templateString, { 22 | srcUrl: JSON.stringify(id), 23 | contentId: JSON.stringify(contentId), 24 | name: JSON.stringify(name), 25 | description: JSON.stringify(description), 26 | components: JSON.stringify(components), 27 | }); 28 | 29 | return { 30 | code, 31 | map: null, 32 | }; 33 | }, 34 | }; -------------------------------------------------------------------------------- /types/jsx.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import url from 'url'; 4 | import Babel from '@babel/core'; 5 | import fetch from 'node-fetch'; 6 | import dataUrls from 'data-urls'; 7 | import {parseIdHash} from '../util.js'; 8 | 9 | const textDecoder = new TextDecoder(); 10 | const cwd = process.cwd(); 11 | 12 | export default { 13 | async resolveId(source, importer) { 14 | if (/^\.\//.test(source) && /^data:/.test(importer)) { 15 | return path.join(cwd, source); 16 | } else { 17 | return undefined; 18 | } 19 | }, 20 | async load(id) { 21 | let src; 22 | if (/https?:/i.test(id)) { 23 | const o = url.parse(id, true); 24 | o.query['noimport'] = 1 + ''; 25 | id = url.format(o); 26 | 27 | const res = await fetch(id); 28 | src = await res.text(); 29 | } else if (/^data:/.test(id)) { 30 | const o = dataUrls(id); 31 | if (o) { 32 | const {/*mimeType, */body} = o; 33 | src = textDecoder.decode(body); 34 | } else { 35 | throw new Error('invalid data url'); 36 | } 37 | } else { 38 | const p = id.replace(/#[\s\S]+$/, ''); 39 | src = await fs.promises.readFile(p, 'utf8'); 40 | } 41 | 42 | const { 43 | contentId, 44 | name, 45 | description, 46 | components, 47 | } = parseIdHash(id); 48 | 49 | const spec = Babel.transform(src, { 50 | presets: ['@babel/preset-react'], 51 | // compact: false, 52 | }); 53 | let {code} = spec; 54 | 55 | code += ` 56 | 57 | export const contentId = ${JSON.stringify(contentId)}; 58 | export const name = ${JSON.stringify(name)}; 59 | export const description = ${JSON.stringify(description)}; 60 | export const type = 'js'; 61 | export const components = ${JSON.stringify(components)}; 62 | `; 63 | return { 64 | code, 65 | map: null, 66 | }; 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /types/light.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'light.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | id = createRelativeFromAbsolutePath(id); 12 | 13 | const { 14 | contentId, 15 | name, 16 | description, 17 | components, 18 | } = parseIdHash(id); 19 | 20 | const code = fillTemplate(templateString, { 21 | srcUrl: JSON.stringify(id), 22 | contentId: JSON.stringify(contentId), 23 | name: JSON.stringify(name), 24 | description: JSON.stringify(description), 25 | components: JSON.stringify(components), 26 | }); 27 | 28 | return { 29 | code, 30 | map: null, 31 | }; 32 | }, 33 | }; -------------------------------------------------------------------------------- /types/lore.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'lore.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | 12 | id = createRelativeFromAbsolutePath(id); 13 | 14 | const { 15 | contentId, 16 | name, 17 | description, 18 | components, 19 | } = parseIdHash(id); 20 | 21 | const code = fillTemplate(templateString, { 22 | srcUrl: JSON.stringify(id), 23 | contentId: JSON.stringify(contentId), 24 | name: JSON.stringify(name), 25 | description: JSON.stringify(description), 26 | components: JSON.stringify(components), 27 | }); 28 | 29 | return { 30 | code, 31 | map: null, 32 | }; 33 | }, 34 | }; -------------------------------------------------------------------------------- /types/metaversefile.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import url from 'url'; 3 | import {fetchFileFromId, createRelativeFromAbsolutePath} from '../util.js'; 4 | 5 | const _jsonParse2 = s => { 6 | try { 7 | const result = JSON.parse(s); 8 | return {result}; 9 | } catch(error) { 10 | return {error}; 11 | } 12 | }; 13 | 14 | export default { 15 | async resolveId(id, importer) { 16 | const s = await fetchFileFromId(id, importer, 'utf8'); 17 | 18 | if (s !== null) { 19 | const {result, error} = _jsonParse2(s); 20 | 21 | if (!error) { 22 | const {name, description, start_url, components} = result; 23 | 24 | if (start_url) { 25 | const _mapUrl = () => { 26 | if (/^https?:\/\//.test(start_url)) { 27 | const o = url.parse(start_url, true); 28 | let s = url.format(o); 29 | return s; 30 | } else if (/^https?:\/\//.test(id)) { 31 | const o = url.parse(id, true); 32 | o.pathname = path.join(path.dirname(o.pathname), start_url); 33 | let s = url.format(o); 34 | return s; 35 | } else if (/^\//.test(id)) { 36 | id = createRelativeFromAbsolutePath(id); 37 | 38 | const o = url.parse(id, true); 39 | o.pathname = path.join(path.dirname(o.pathname), start_url); 40 | let s = url.format(o); 41 | if (/^\//.test(s)) { 42 | const cwd = process.cwd(); 43 | s = cwd + s; 44 | } 45 | return s; 46 | } else { 47 | console.warn('.metaversefile scheme unknown'); 48 | return null; 49 | } 50 | }; 51 | const _makeHash = mapped_start_url => { 52 | const searchParams = new URLSearchParams(); 53 | 54 | searchParams.set('contentId', mapped_start_url); 55 | if (name) { 56 | searchParams.set('name', name); 57 | } 58 | if (description) { 59 | searchParams.set('description', description); 60 | } 61 | if (Array.isArray(components)) { 62 | searchParams.set('components', JSON.stringify(components)); 63 | } 64 | const s = searchParams.toString(); 65 | return s ? ('#' + s) : ''; 66 | }; 67 | 68 | let u = _mapUrl(); 69 | if (u) { 70 | u += _makeHash(u); 71 | return u; 72 | } else { 73 | return u; 74 | } 75 | } else { 76 | console.warn('.metaversefile has no "start_url": string', {j, id, s}); 77 | return null; 78 | } 79 | } else { 80 | console.warn('.metaversefile could not be parsed: ' + error.stack); 81 | return null; 82 | } 83 | } else { 84 | console.warn('.metaversefile could not be loaded'); 85 | return null; 86 | } 87 | } 88 | }; -------------------------------------------------------------------------------- /types/mob.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'mob.js'), 'utf8'); 7 | 8 | export default { 9 | load(id) { 10 | id = createRelativeFromAbsolutePath(id); 11 | 12 | const { 13 | contentId, 14 | name, 15 | description, 16 | components, 17 | } = parseIdHash(id); 18 | 19 | const code = fillTemplate(templateString, { 20 | srcUrl: JSON.stringify(id), 21 | contentId: JSON.stringify(contentId), 22 | name: JSON.stringify(name), 23 | description: JSON.stringify(description), 24 | components: JSON.stringify(components), 25 | }); 26 | return { 27 | code, 28 | map: null, 29 | }; 30 | }, 31 | }; -------------------------------------------------------------------------------- /types/npc.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'npc.js'), 'utf8'); 7 | 8 | export default { 9 | load(id) { 10 | id = createRelativeFromAbsolutePath(id); 11 | 12 | const { 13 | contentId, 14 | name, 15 | description, 16 | components, 17 | } = parseIdHash(id); 18 | 19 | const code = fillTemplate(templateString, { 20 | srcUrl: JSON.stringify(id), 21 | contentId: JSON.stringify(contentId), 22 | name: JSON.stringify(name), 23 | description: JSON.stringify(description), 24 | components: JSON.stringify(components), 25 | }); 26 | return { 27 | code, 28 | map: null, 29 | }; 30 | }, 31 | }; -------------------------------------------------------------------------------- /types/quest.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'quest.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | 12 | id = createRelativeFromAbsolutePath(id); 13 | 14 | const { 15 | contentId, 16 | name, 17 | description, 18 | components, 19 | } = parseIdHash(id); 20 | 21 | const code = fillTemplate(templateString, { 22 | srcUrl: JSON.stringify(id), 23 | contentId: JSON.stringify(contentId), 24 | name: JSON.stringify(name), 25 | description: JSON.stringify(description), 26 | components: JSON.stringify(components), 27 | }); 28 | 29 | return { 30 | code, 31 | map: null, 32 | }; 33 | }, 34 | }; -------------------------------------------------------------------------------- /types/react.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'react.js'), 'utf8'); 7 | 8 | export default { 9 | load(id) { 10 | 11 | id = createRelativeFromAbsolutePath(id); 12 | 13 | const { 14 | contentId, 15 | name, 16 | description, 17 | components, 18 | } = parseIdHash(id); 19 | 20 | const code = fillTemplate(templateString, { 21 | srcUrl: JSON.stringify(id), 22 | contentId: JSON.stringify(contentId), 23 | name: JSON.stringify(name), 24 | description: JSON.stringify(description), 25 | components: JSON.stringify(components), 26 | }); 27 | 28 | return { 29 | code, 30 | map: null, 31 | }; 32 | }, 33 | }; -------------------------------------------------------------------------------- /types/rendersettings.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'rendersettings.js'), 'utf8'); 7 | 8 | export default { 9 | load(id) { 10 | 11 | id = createRelativeFromAbsolutePath(id); 12 | 13 | const { 14 | contentId, 15 | name, 16 | description, 17 | components, 18 | } = parseIdHash(id); 19 | 20 | const code = fillTemplate(templateString, { 21 | srcUrl: JSON.stringify(id), 22 | contentId: JSON.stringify(contentId), 23 | name: JSON.stringify(name), 24 | description: JSON.stringify(description), 25 | components: JSON.stringify(components), 26 | }); 27 | 28 | return { 29 | code, 30 | map: null, 31 | }; 32 | }, 33 | }; -------------------------------------------------------------------------------- /types/scn.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'scn.js'), 'utf8'); 7 | 8 | export default { 9 | load(id) { 10 | id = createRelativeFromAbsolutePath(id); 11 | 12 | const { 13 | contentId, 14 | name, 15 | description, 16 | components, 17 | } = parseIdHash(id); 18 | 19 | const code = fillTemplate(templateString, { 20 | srcUrl: JSON.stringify(id), 21 | contentId: JSON.stringify(contentId), 22 | name: JSON.stringify(name), 23 | description: JSON.stringify(description), 24 | components: JSON.stringify(components), 25 | }); 26 | 27 | return { 28 | code, 29 | map: null, 30 | }; 31 | }, 32 | }; -------------------------------------------------------------------------------- /types/spawnpoint.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'spawnpoint.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | 12 | id = createRelativeFromAbsolutePath(id); 13 | 14 | const { 15 | contentId, 16 | name, 17 | description, 18 | components, 19 | } = parseIdHash(id); 20 | 21 | const code = fillTemplate(templateString, { 22 | srcUrl: JSON.stringify(id), 23 | contentId: JSON.stringify(contentId), 24 | name: JSON.stringify(name), 25 | description: JSON.stringify(description), 26 | components: JSON.stringify(components), 27 | }); 28 | 29 | return { 30 | code, 31 | map: null, 32 | }; 33 | }, 34 | }; -------------------------------------------------------------------------------- /types/text.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'text.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | 12 | id = createRelativeFromAbsolutePath(id); 13 | 14 | const { 15 | contentId, 16 | name, 17 | description, 18 | components, 19 | } = parseIdHash(id); 20 | 21 | const code = fillTemplate(templateString, { 22 | srcUrl: JSON.stringify(id), 23 | contentId: JSON.stringify(contentId), 24 | name: JSON.stringify(name), 25 | description: JSON.stringify(description), 26 | components: JSON.stringify(components), 27 | }); 28 | 29 | return { 30 | code, 31 | map: null, 32 | }; 33 | }, 34 | }; -------------------------------------------------------------------------------- /types/vircadia.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'vircadia.js'), 'utf8'); 7 | 8 | export default { 9 | load(id) { 10 | id = createRelativeFromAbsolutePath(id); 11 | 12 | const { 13 | contentId, 14 | name, 15 | description, 16 | components, 17 | } = parseIdHash(id); 18 | 19 | const code = fillTemplate(templateString, { 20 | srcUrl: JSON.stringify(id), 21 | contentId: JSON.stringify(contentId), 22 | name: JSON.stringify(name), 23 | description: JSON.stringify(description), 24 | components: JSON.stringify(components), 25 | }); 26 | 27 | return { 28 | code, 29 | map: null, 30 | }; 31 | }, 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /types/vox.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'vox.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | id = createRelativeFromAbsolutePath(id); 12 | 13 | const { 14 | contentId, 15 | name, 16 | description, 17 | components, 18 | } = parseIdHash(id); 19 | 20 | const code = fillTemplate(templateString, { 21 | srcUrl: JSON.stringify(id), 22 | contentId: JSON.stringify(contentId), 23 | name: JSON.stringify(name), 24 | description: JSON.stringify(description), 25 | components: JSON.stringify(components), 26 | }); 27 | 28 | return { 29 | code, 30 | map: null, 31 | }; 32 | }, 33 | }; -------------------------------------------------------------------------------- /types/vrm.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'vrm.js'), 'utf8'); 7 | 8 | export default { 9 | load(id) { 10 | id = createRelativeFromAbsolutePath(id); 11 | 12 | const { 13 | contentId, 14 | name, 15 | description, 16 | components, 17 | } = parseIdHash(id); 18 | 19 | const code = fillTemplate(templateString, { 20 | srcUrl: JSON.stringify(id), 21 | contentId: JSON.stringify(contentId), 22 | name: JSON.stringify(name), 23 | description: JSON.stringify(description), 24 | components: JSON.stringify(components), 25 | }); 26 | return { 27 | code, 28 | map: null, 29 | }; 30 | }, 31 | }; -------------------------------------------------------------------------------- /types/wind.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import {fillTemplate, createRelativeFromAbsolutePath, parseIdHash} from '../util.js'; 4 | 5 | const dirname = path.dirname(import.meta.url.replace(/^[a-z]+:\/\//, '')); 6 | const templateString = fs.readFileSync(path.join(dirname, '..', 'type_templates', 'wind.js'), 'utf8'); 7 | // const cwd = process.cwd(); 8 | 9 | export default { 10 | load(id) { 11 | 12 | id = createRelativeFromAbsolutePath(id); 13 | 14 | const { 15 | contentId, 16 | name, 17 | description, 18 | components, 19 | } = parseIdHash(id); 20 | 21 | const code = fillTemplate(templateString, { 22 | srcUrl: JSON.stringify(id), 23 | contentId: JSON.stringify(contentId), 24 | name: JSON.stringify(name), 25 | description: JSON.stringify(description), 26 | components: JSON.stringify(components), 27 | }); 28 | 29 | return { 30 | code, 31 | map: null, 32 | }; 33 | }, 34 | }; -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import url from 'url'; 4 | 5 | export function jsonParse(s) { 6 | try { 7 | return JSON.parse(s); 8 | } catch (err) { 9 | return null; 10 | } 11 | } 12 | 13 | export const resolveFileFromId = (id, importer) => { 14 | id = id.replace(/^[\/\\]+/, ''); 15 | let match; 16 | // console.log('load', id, match); 17 | if (match = id.match(/^ipfs:\/+([a-z0-9]+)((?:\/?[^\/\?]*)*)(\?\.(.+))?$/i)) { 18 | return `https://ipfs.webaverse.com/ipfs/${match[1]}${match[2]}`; 19 | } else if (match = id.match(/^\/@proxy\/(.+)$/)) { 20 | return match[1]; 21 | } else { 22 | return null; 23 | } 24 | }; 25 | 26 | export const fetchFileFromId = async (id, importer, encoding = null) => { 27 | id = id 28 | .replace(/^\/@proxy\//, '') 29 | .replace(/^(https?:\/(?!\/))/, '$1/'); 30 | if (/^https?:\/\//.test(id)) { 31 | const u = url.parse(id, true); 32 | u.query.noimport = 1 + ''; 33 | id = url.format(u); 34 | const res = await fetch(id) 35 | if (encoding === 'utf8') { 36 | const s = await res.text(); 37 | return s; 38 | } else { 39 | const arrayBuffer = await res.arrayBuffer(); 40 | const buffer = Buffer.from(arrayBuffer); 41 | return buffer; 42 | } 43 | } else { 44 | return await new Promise((accept, reject) => { 45 | const cwd = process.cwd(); 46 | const p = path.join(cwd, id.replace(/^[\/\\]+/, '')); 47 | // console.log('read dir', {id, importer, p}); 48 | fs.readFile(p, encoding, (err, d) => { 49 | if (!err) { 50 | accept(d); 51 | } else { 52 | if (err.code === 'ENOENT') { 53 | accept(null); 54 | } else { 55 | reject(err); 56 | } 57 | } 58 | }); 59 | }); 60 | } 61 | }; 62 | 63 | export const fillTemplate = function(templateString, templateVars) { 64 | return new Function("return `"+templateString +"`;").call(templateVars); 65 | }; 66 | 67 | export const createRelativeFromAbsolutePath = path => { 68 | const cwd = process.cwd(); 69 | if (path.startsWith(cwd.replaceAll('\\','/'))) { 70 | path = path.slice(cwd.length); 71 | } 72 | return path; 73 | } 74 | 75 | export const parseIdHash = id => { 76 | let contentId = ''; 77 | let name = ''; 78 | let description = ''; 79 | let components = []; 80 | 81 | const match = id.match(/#([\s\S]+)$/); 82 | if (match) { 83 | const q = new URLSearchParams(match[1]); 84 | const qContentId = q.get('contentId'); 85 | if (qContentId !== undefined) { 86 | contentId = qContentId; 87 | } 88 | const qName = q.get('name'); 89 | if (qName !== undefined) { 90 | name = qName; 91 | } 92 | const qDescription = q.get('description'); 93 | if (qDescription !== undefined) { 94 | description = qDescription; 95 | } 96 | const qComponents = q.get('components'); 97 | if (qComponents !== undefined) { 98 | components = jsonParse(qComponents) ?? []; 99 | } 100 | } 101 | if (!contentId) { 102 | contentId = id.match(/^([^#]*)/)[1]; 103 | } 104 | if (!name) { 105 | if (/^data:/.test(contentId)) { 106 | name = contentId.match(/^data:([^\;\,]*)/)[1]; 107 | } else { 108 | name = contentId.match(/([^\/\.]*)(?:\.[a-zA-Z0-9]*)?$/)[1]; 109 | } 110 | } 111 | 112 | return { 113 | contentId, 114 | name, 115 | description, 116 | components, 117 | }; 118 | }; --------------------------------------------------------------------------------