From 6a8f30bb209cec74ca34965de5b8a3fb296bb89e Mon Sep 17 00:00:00 2001 From: saori-eth Date: Sat, 21 Jun 2025 17:57:12 -0500 Subject: [PATCH 1/2] primitives with physics --- docs/README.md | 5 +- docs/primitives.md | 279 +++++++++++++++++++++++++++++++++++ docs/scripts.md | 64 +++++++- example-primitive-app.js | 47 ++++++ physics-primitive-test.js | 116 +++++++++++++++ primitive-app-setup.md | 68 +++++++++ src/core/nodes/Mesh.js | 150 ++++++++++++++++++- src/core/systems/Apps.js | 91 ++++++++++++ test-primitives.js | 65 ++++++++ visible-primitive-test.js | 81 ++++++++++ working-primitive-example.js | 29 ++++ 11 files changed, 986 insertions(+), 9 deletions(-) create mode 100644 docs/primitives.md create mode 100644 example-primitive-app.js create mode 100644 physics-primitive-test.js create mode 100644 primitive-app-setup.md create mode 100644 test-primitives.js create mode 100644 visible-primitive-test.js create mode 100644 working-primitive-example.js diff --git a/docs/README.md b/docs/README.md index eddead6f..7a20449b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,4 +4,7 @@ These docs are for 3D artists and developers looking to build assets and/or full - [Commands](/docs/commands.md) - [Models](/docs/models.md) -- [Scripts](/docs/scripts.md) \ No newline at end of file +- [Scripts](/docs/scripts.md) +- [Primitives](/docs/primitives.md) +- [HYP Format](/docs/hyp-format.md) +- [Blender Exporter](/docs/blender-exporter.md) \ No newline at end of file diff --git a/docs/primitives.md b/docs/primitives.md new file mode 100644 index 00000000..3c442661 --- /dev/null +++ b/docs/primitives.md @@ -0,0 +1,279 @@ +# Primitives + +Hyperfy provides convenient methods to create primitive meshes directly from scripts without needing to load external 3D models. These primitives are lightweight, instanced objects that integrate seamlessly with the physics and rendering systems. + +## Available Primitives + +### Box + +Creates a rectangular box mesh. + +```js +const box = world.box({ + width: 1, // Width along X axis (default: 1) + height: 1, // Height along Y axis (default: 1) + depth: 1 // Depth along Z axis (default: 1) +}) +``` + +### Sphere + +Creates a spherical mesh. + +```js +const sphere = world.sphere({ + radius: 0.5 // Sphere radius (default: 0.5) +}) +``` + +### Cylinder + +Creates a cylindrical mesh. + +```js +const cylinder = world.cylinder({ + radiusTop: 0.5, // Top radius (default: 0.5) + radiusBottom: 0.5, // Bottom radius (default: 0.5) + height: 1, // Height along Y axis (default: 1) + radialSegments: 8 // Number of segments around circumference (default: 8) +}) +``` + +### Cone + +Creates a conical mesh. + +```js +const cone = world.cone({ + radius: 0.5, // Base radius (default: 0.5) + height: 1, // Height along Y axis (default: 1) + radialSegments: 8 // Number of segments around circumference (default: 8) +}) +``` + +## Generic Method + +You can also use the generic `spawnMesh` method to create any primitive type: + +```js +const mesh = world.spawnMesh({ + type: 'box', // Required: 'box', 'sphere', 'cylinder', or 'cone' + width: 2, // Type-specific parameters + height: 1, + depth: 1 +}) +``` + +## Common Properties + +All primitives are [Mesh nodes](/docs/ref/Mesh.md) and inherit standard node properties: + +### Transform Properties +```js +// Position +primitive.position.set(x, y, z) +primitive.position.x = 5 + +// Rotation (in radians) +primitive.rotation.set(x, y, z) +primitive.rotation.y = Math.PI / 2 + +// Scale +primitive.scale.set(x, y, z) +primitive.scale.setScalar(2) // Uniform scale + +// Quaternion (alternative to rotation) +primitive.quaternion.setFromEuler(euler) +``` + +### Material Properties +```js +// Color (string format: named colors, hex, rgb, hsl) +primitive.material.color = 'red' +primitive.material.color = '#ff0000' +primitive.material.color = 'rgb(255, 0, 0)' + +// Emissive (self-illumination) +primitive.material.emissive = '#003300' +primitive.material.emissiveIntensity = 0.5 + +// Other material properties +primitive.material.metalness = 0.5 // 0-1, default: 0 +primitive.material.roughness = 0.7 // 0-1, default: 1 +primitive.material.fog = false // Affected by fog, default: true +``` + +### Visibility and Shadows +```js +primitive.active = false // Hide/show the primitive +primitive.castShadow = true // Cast shadows (default: true) +primitive.receiveShadow = true // Receive shadows (default: true) +``` + +## Physics (RigidBody) + +You can optionally create primitives with physics enabled by adding a `rigidbody` option. This automatically creates a rigidbody parent with a matching collider: + +```js +// Simple physics - creates a dynamic rigidbody +const physicsBox = world.box({ + width: 1, + height: 1, + depth: 1, + rigidbody: true // Creates dynamic rigidbody with default settings +}) + +// Advanced physics configuration +const physicsSphere = world.sphere({ + radius: 0.5, + rigidbody: { + type: 'dynamic', // 'static', 'kinematic', or 'dynamic' + mass: 2, // Mass in kg (default: 1) + linearDamping: 0.1, // Linear velocity damping (default: 0) + angularDamping: 0.5, // Angular velocity damping (default: 0.05) + layer: 'prop', // Collision layer (default: 'prop') + trigger: false, // Is it a trigger? (default: false) + convex: false, // Use convex collider? (default: false) + tag: 'pickup', // Custom tag for identification + onContactStart: (other) => { + console.log('Contact started with:', other.tag) + }, + onContactEnd: (other) => { + console.log('Contact ended with:', other.tag) + } + } +}) + +// The returned object is the rigidbody, with the mesh as a child +physicsBox.position.set(0, 5, -3) // Drop from height +physicsBox.setLinearVelocity(new Vector3(0, -1, 0)) +``` + +### Physics Properties + +When `rigidbody` is enabled, the returned object is the rigidbody node (not the mesh). The mesh becomes a child of the rigidbody. This gives you access to physics methods: + +```js +// Physics methods +primitive.addForce(force, mode) +primitive.addTorque(torque, mode) +primitive.setLinearVelocity(velocity) +primitive.setAngularVelocity(velocity) +primitive.getLinearVelocity() +primitive.getAngularVelocity() + +// Access the mesh child for visual properties +primitive.children[0].material.color = 'red' // First child is the mesh +``` + +### Collider Shapes + +- **Box**: Exact box collider matching dimensions +- **Sphere**: Exact sphere collider matching radius +- **Cylinder/Cone**: Currently use box approximation (cylinder/cone colliders coming soon) + +## Cloning Primitives + +Yes, you can clone primitives! Use the standard node cloning method: + +```js +// Create original +const originalBox = world.box({ width: 1, height: 2, depth: 1 }) +originalBox.position.set(0, 1, -5) +originalBox.material.color = 'blue' + +// Clone it +const clonedBox = originalBox.clone() +clonedBox.position.set(2, 1, -5) // Position the clone elsewhere +clonedBox.material.color = 'red' // Give it a different color + +// Add the clone to the world +world.add(clonedBox) +``` + +### Deep Cloning + +By default, `clone()` creates a shallow copy. For a deep clone (including all children): + +```js +const deepClone = originalNode.clone(true) // true = recursive +``` + +## Complete Example + +```js +export default { + init() { + // Create a variety of primitives + this.primitives = [] + + // Rotating box + const box = world.box({ width: 1, height: 1, depth: 1 }) + box.position.set(-3, 1, -5) + box.material.color = '#ff0000' + box.material.emissiveIntensity = 0.2 + this.primitives.push(box) + + // Metallic sphere + const sphere = world.sphere({ radius: 0.5 }) + sphere.position.set(-1, 1, -5) + sphere.material.color = '#0080ff' + sphere.material.metalness = 1 + sphere.material.roughness = 0.2 + this.primitives.push(sphere) + + // Tapered cylinder (different top/bottom radii) + const cylinder = world.cylinder({ + radiusTop: 0.2, + radiusBottom: 0.5, + height: 1.5, + radialSegments: 16 // Smoother cylinder + }) + cylinder.position.set(1, 0.75, -5) + cylinder.material.color = '#00ff00' + this.primitives.push(cylinder) + + // Clone the sphere + const clonedSphere = sphere.clone() + clonedSphere.position.set(3, 1, -5) + clonedSphere.material.color = '#ff00ff' + world.add(clonedSphere) + this.primitives.push(clonedSphere) + }, + + update() { + // Animate primitives + const time = world.getTime() * 0.001 + this.primitives.forEach((prim, i) => { + prim.rotation.y = time + (i * Math.PI / 2) + }) + }, + + destroy() { + // Clean up all primitives + this.primitives.forEach(prim => { + world.remove(prim) + }) + } +} +``` + +## Performance Considerations + +1. **Instancing**: Primitives with identical geometry and material properties are automatically instanced for better performance +2. **Geometry Caching**: Primitive geometries are cached and reused to minimize memory usage +3. **Material Sharing**: Use the same material properties when possible to benefit from instancing +4. **Cleanup**: Always remove primitives in the `destroy()` method to prevent memory leaks + +## Limitations + +1. **Material Changes**: You cannot replace the entire material object, only modify its properties +2. **Geometry Modifications**: Primitive geometry cannot be modified after creation (create a new primitive instead) +3. **Custom Shaders**: Primitives use the standard material system and don't support custom shaders directly + +## Tips + +- Primitives spawn at origin (0,0,0) by default - always set their position +- Use hex strings for consistent color representation +- For invisible colliders, create a primitive and set `primitive.active = false` +- Combine multiple primitives using parent-child relationships for complex shapes \ No newline at end of file diff --git a/docs/scripts.md b/docs/scripts.md index f6c9752c..7c144090 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -42,4 +42,66 @@ Some nodes can also be created and used on the fly using `app.create(nodeName)`. - [Controller](/docs/ref/Controller.md) - [RigidBody](/docs/ref/RigidBody.md) - [Collider](/docs/ref/Collider.md) -- [Joint](/docs/ref/Joint.md) \ No newline at end of file +- [Joint](/docs/ref/Joint.md) + +## Creating Primitives + +Hyperfy provides convenient methods to create primitive meshes directly from scripts without needing to load external models. + +### Basic Usage + +```js +// Create a box +const cube = world.box({ width: 1, height: 1, depth: 1 }) +cube.position.set(0, 0.5, -2) +cube.material.color = 'red' + +// Create a sphere +const sphere = world.sphere({ radius: 0.5 }) +sphere.position.set(2, 0.5, -2) +sphere.material.color = 'blue' + +// Create a cylinder +const cylinder = world.cylinder({ + radiusTop: 0.3, + radiusBottom: 0.5, + height: 1.5, + radialSegments: 8 +}) +cylinder.position.set(-2, 0.75, -2) +cylinder.material.color = 'green' + +// Create a cone +const cone = world.cone({ + radius: 0.5, + height: 1, + radialSegments: 8 +}) +cone.position.set(0, 0.5, -4) +cone.material.color = 'yellow' +``` + +### Generic Method + +You can also use the generic `spawnMesh` method: + +```js +const mesh = world.spawnMesh({ + type: 'sphere', // 'box', 'sphere', 'cylinder', or 'cone' + radius: 0.5 // plus any type-specific parameters +}) +``` + +### Parameters + +- **Box**: `width`, `height`, `depth` +- **Sphere**: `radius` +- **Cylinder**: `radiusTop`, `radiusBottom`, `height`, `radialSegments` +- **Cone**: `radius`, `height`, `radialSegments` + +### Notes + +- Meshes created this way are lightweight instanced objects +- They obey normal physics and ray-cast rules +- They use the default unlit material which can be modified via the material proxy +- Remember to remove primitives when no longer needed: `world.remove(mesh)` \ No newline at end of file diff --git a/example-primitive-app.js b/example-primitive-app.js new file mode 100644 index 00000000..d3454faf --- /dev/null +++ b/example-primitive-app.js @@ -0,0 +1,47 @@ +// Example app script that uses primitive spawning +// This should be used as the script content for an app, not as the model URL + +export default { + init() { + // Create primitives when the app initializes + this.primitives = [] + + // Create a red box + const box = world.box({ width: 1, height: 1, depth: 1 }) + box.position.set(0, 0.5, 0) + box.material.color = 'red' + this.primitives.push(box) + + // Create a blue sphere + const sphere = world.sphere({ radius: 0.5 }) + sphere.position.set(2, 0.5, 0) + sphere.material.color = 'blue' + this.primitives.push(sphere) + + // Create a green cylinder + const cylinder = world.cylinder({ + radiusTop: 0.3, + radiusBottom: 0.5, + height: 1.5, + radialSegments: 8 + }) + cylinder.position.set(-2, 0.75, 0) + cylinder.material.color = 'green' + this.primitives.push(cylinder) + }, + + update() { + // Rotate primitives + const time = world.getTime() * 0.001 + this.primitives.forEach((primitive, i) => { + primitive.rotation.y = time + i + }) + }, + + destroy() { + // Clean up primitives when app is destroyed + this.primitives.forEach(primitive => { + world.remove(primitive) + }) + } +} \ No newline at end of file diff --git a/physics-primitive-test.js b/physics-primitive-test.js new file mode 100644 index 00000000..b0bd9fb3 --- /dev/null +++ b/physics-primitive-test.js @@ -0,0 +1,116 @@ +// Test script for primitives with physics + +export default { + init() { + console.log('Creating physics primitives...') + + this.primitives = [] + + // Create a static floor + const floor = world.box({ + width: 20, + height: 0.1, + depth: 20, + rigidbody: { + type: 'static' + } + }) + floor.position.set(0, 0, -5) + floor.children[0].material.color = '#444444' + this.primitives.push(floor) + + // Create falling boxes with different masses + for (let i = 0; i < 3; i++) { + const box = world.box({ + width: 1, + height: 1, + depth: 1, + rigidbody: { + type: 'dynamic', + mass: 0.5 + i * 0.5, // 0.5, 1.0, 1.5 kg + linearDamping: 0.1, + angularDamping: 0.5 + } + }) + box.position.set(-2 + i * 2, 3 + i, -5) + box.children[0].material.color = `hsl(${i * 120}, 100%, 50%)` + this.primitives.push(box) + } + + // Create a bouncy ball + const ball = world.sphere({ + radius: 0.5, + rigidbody: { + type: 'dynamic', + mass: 0.3, + layer: 'prop', + tag: 'ball' + } + }) + ball.position.set(0, 5, -5) + ball.children[0].material.color = '#ff00ff' + ball.setLinearVelocity(new Vector3(2, 0, 0)) + this.primitives.push(ball) + + // Create a trigger zone + const trigger = world.box({ + width: 3, + height: 3, + depth: 3, + rigidbody: { + type: 'static', + trigger: true, + onTriggerEnter: (other) => { + console.log('Something entered trigger:', other.tag) + trigger.children[0].material.color = '#00ff00' + }, + onTriggerLeave: (other) => { + console.log('Something left trigger:', other.tag) + trigger.children[0].material.color = '#ff000080' + } + } + }) + trigger.position.set(5, 1.5, -5) + trigger.children[0].material.color = '#ff000080' + trigger.children[0].material.emissiveIntensity = 0.5 + this.primitives.push(trigger) + + // Create a kinematic platform + const platform = world.box({ + width: 3, + height: 0.5, + depth: 2, + rigidbody: { + type: 'kinematic' + } + }) + platform.position.set(-5, 2, -5) + platform.children[0].material.color = '#00ff00' + this.primitives.push(platform) + this.platform = platform + + console.log('Created', this.primitives.length, 'physics primitives') + }, + + update() { + // Animate the kinematic platform + if (this.platform) { + const time = world.getTime() * 0.001 + const newPos = new Vector3( + -5 + Math.sin(time) * 3, + 2 + Math.sin(time * 2) * 0.5, + -5 + ) + this.platform.setKinematicTarget(newPos, this.platform.quaternion) + } + }, + + destroy() { + console.log('Cleaning up physics primitives...') + if (this.primitives) { + this.primitives.forEach(prim => { + world.remove(prim) + }) + } + } +} \ No newline at end of file diff --git a/primitive-app-setup.md b/primitive-app-setup.md new file mode 100644 index 00000000..707be96b --- /dev/null +++ b/primitive-app-setup.md @@ -0,0 +1,68 @@ +# How to Use Primitive Spawning in Hyperfy Apps + +## Important: Understanding the Architecture + +Apps in Hyperfy typically have: +1. A **model** (GLB file) - The 3D content +2. A **script** (JS file) - The behavior/logic + +For primitive spawning, you don't need a model file since primitives are created dynamically by the script. + +## Method 1: Create an App with Empty/Minimal Model + +1. Create a minimal GLB file (can be an empty scene or a single invisible object) +2. Create your script that spawns primitives +3. Drop the GLB file into the world to create the app +4. Add your script to the app + +## Method 2: Use Console for Testing + +You can test primitive spawning directly in the browser console: + +```js +// Access the world object +const w = window.world + +// Create a box +const box = w.box({ width: 1, height: 1, depth: 1 }) +box.position.set(0, 1, -5) +box.material.color = 'red' + +// Create a sphere +const sphere = w.sphere({ radius: 0.5 }) +sphere.position.set(2, 1, -5) +sphere.material.color = 'blue' + +// Remove them later +w.remove(box) +w.remove(sphere) +``` + +## Method 3: Modify an Existing App + +1. Select an existing app in your world +2. Open the script editor +3. Replace or modify the script to include primitive spawning: + +```js +export default { + init() { + // Your existing code... + + // Add primitive spawning + const cube = world.box({ width: 0.5, height: 0.5, depth: 0.5 }) + cube.position.set(0, 1, 0) + cube.material.color = 'yellow' + } +} +``` + +## Common Issues + +1. **"primitive" is not a valid model URL** - Don't set the app's model to "primitive", use a real GLB file +2. **Primitives not showing** - Make sure to set position, primitives spawn at origin (0,0,0) by default +3. **Memory leaks** - Always remove primitives in the `destroy()` method + +## Example Script + +See `example-primitive-app.js` for a complete working example that demonstrates all primitive types with animation. \ No newline at end of file diff --git a/src/core/nodes/Mesh.js b/src/core/nodes/Mesh.js index 8c1b3443..e79483cb 100644 --- a/src/core/nodes/Mesh.js +++ b/src/core/nodes/Mesh.js @@ -14,6 +14,9 @@ const defaults = { height: 1, depth: 1, radius: 0.5, + radiusTop: 0.5, + radiusBottom: 0.5, + radialSegments: 8, geometry: null, material: null, linked: true, @@ -22,7 +25,7 @@ const defaults = { visible: true, // DEPRECATED: use Node.active } -const types = ['box', 'sphere', 'geometry'] +const types = ['box', 'sphere', 'cylinder', 'cone', 'geometry'] let boxes = {} const getBox = (width, height, depth) => { @@ -42,6 +45,24 @@ const getSphere = radius => { return spheres[key] } +let cylinders = {} +const getCylinder = (radiusTop, radiusBottom, height, radialSegments) => { + const key = `${radiusTop},${radiusBottom},${height},${radialSegments}` + if (!cylinders[key]) { + cylinders[key] = new THREE.CylinderGeometry(radiusTop, radiusBottom, height, radialSegments) + } + return cylinders[key] +} + +let cones = {} +const getCone = (radius, height, radialSegments) => { + const key = `${radius},${height},${radialSegments}` + if (!cones[key]) { + cones[key] = new THREE.ConeGeometry(radius, height, radialSegments) + } + return cones[key] +} + export class Mesh extends Node { constructor(data = {}) { super(data) @@ -52,40 +73,70 @@ export class Mesh extends Node { this.height = data.height this.depth = data.depth this.radius = data.radius + this.radiusTop = data.radiusTop + this.radiusBottom = data.radiusBottom + this.radialSegments = data.radialSegments this.geometry = data.geometry this.material = data.material this.linked = data.linked this.castShadow = data.castShadow this.receiveShadow = data.receiveShadow this.visible = data.visible + + // Store pending material properties until handle is created + this._pendingMaterialProps = {} } mount() { this.needsRebuild = false - if (!this._geometry) return let geometry if (this._type === 'box') { geometry = getBox(this._width, this._height, this._depth) } else if (this._type === 'sphere') { geometry = getSphere(this._radius) + } else if (this._type === 'cylinder') { + geometry = getCylinder(this._radiusTop, this._radiusBottom, this._height, this._radialSegments) + } else if (this._type === 'cone') { + geometry = getCone(this._radius, this._height, this._radialSegments) } else if (this._type === 'geometry') { geometry = this._geometry } + + // Ensure we have geometry + if (!geometry) { + console.warn('[Mesh] No geometry available for type:', this._type) + return + } + // Ensure we have a material, create default if needed + let material = this._material + if (!material) { + const defaultMat = this.ctx.world.stage.getDefaultMaterial() + material = defaultMat.raw // Use the raw THREE.js material + } if (this._visible) { this.handle = this.ctx.world.stage.insert({ geometry, - material: this._material, + material, linked: this._linked, castShadow: this._castShadow, receiveShadow: this._receiveShadow, matrix: this.matrixWorld, node: this, }) + // Apply any pending material properties + if (this.handle && this.handle.material && this._pendingMaterialProps) { + for (const [key, value] of Object.entries(this._pendingMaterialProps)) { + if (key in this.handle.material) { + this.handle.material[key] = value + } + } + this._pendingMaterialProps = {} + } } else { this.sItem = { matrix: this.matrixWorld, geometry, - material: this._material, + material, getEntity: () => this.ctx.entity, node: this, } @@ -125,6 +176,9 @@ export class Mesh extends Node { this._height = source._height this._depth = source._depth this._radius = source._radius + this._radiusTop = source._radiusTop + this._radiusBottom = source._radiusBottom + this._radialSegments = source._radialSegments this._geometry = source._geometry this._material = source._material this._linked = source._linked @@ -187,7 +241,7 @@ export class Mesh extends Node { } if (this._height === value) return this._height = value - if (this.handle && this._type === 'box') { + if (this.handle && (this._type === 'box' || this._type === 'cylinder' || this._type === 'cone')) { this.needsRebuild = true this.setDirty() } @@ -225,7 +279,55 @@ export class Mesh extends Node { } if (this._radius === value) return this._radius = value - if (this.handle && this._type === 'sphere') { + if (this.handle && (this._type === 'sphere' || this._type === 'cone')) { + this.needsRebuild = true + this.setDirty() + } + } + + get radiusTop() { + return this._radiusTop + } + + set radiusTop(value = defaults.radiusTop) { + if (!isNumber(value)) { + throw new Error('[mesh] radiusTop not a number') + } + if (this._radiusTop === value) return + this._radiusTop = value + if (this.handle && this._type === 'cylinder') { + this.needsRebuild = true + this.setDirty() + } + } + + get radiusBottom() { + return this._radiusBottom + } + + set radiusBottom(value = defaults.radiusBottom) { + if (!isNumber(value)) { + throw new Error('[mesh] radiusBottom not a number') + } + if (this._radiusBottom === value) return + this._radiusBottom = value + if (this.handle && this._type === 'cylinder') { + this.needsRebuild = true + this.setDirty() + } + } + + get radialSegments() { + return this._radialSegments + } + + set radialSegments(value = defaults.radialSegments) { + if (!isNumber(value)) { + throw new Error('[mesh] radialSegments not a number') + } + if (this._radialSegments === value) return + this._radialSegments = value + if (this.handle && (this._type === 'cylinder' || this._type === 'cone')) { this.needsRebuild = true this.setDirty() } @@ -356,6 +458,24 @@ export class Mesh extends Node { set radius(value) { self.radius = value }, + get radiusTop() { + return self.radiusTop + }, + set radiusTop(value) { + self.radiusTop = value + }, + get radiusBottom() { + return self.radiusBottom + }, + set radiusBottom(value) { + self.radiusBottom = value + }, + get radialSegments() { + return self.radialSegments + }, + set radialSegments(value) { + self.radialSegments = value + }, get geometry() { return self.geometry }, @@ -363,7 +483,23 @@ export class Mesh extends Node { self.geometry = value }, get material() { - return self.material + // If handle exists, return the actual material proxy + if (self.handle && self.handle.material) { + return self.handle.material + } + // Otherwise return a temporary proxy that stores properties + if (!self._tempMaterialProxy) { + self._tempMaterialProxy = new Proxy({}, { + set(target, prop, value) { + self._pendingMaterialProps[prop] = value + return true + }, + get(target, prop) { + return self._pendingMaterialProps[prop] + } + }) + } + return self._tempMaterialProxy }, set material(value) { throw new Error('[mesh] set material not supported') diff --git a/src/core/systems/Apps.js b/src/core/systems/Apps.js index 3e7110dd..f7c653e6 100644 --- a/src/core/systems/Apps.js +++ b/src/core/systems/Apps.js @@ -213,6 +213,97 @@ export class Apps extends System { } }) }, + spawnMesh(entity, opts = {}) { + const node = entity.createNode('mesh', opts) + self.worldMethods.add(entity, node) + + // Optionally add rigidbody with matching collider + if (opts.rigidbody) { + const rbOpts = typeof opts.rigidbody === 'object' ? opts.rigidbody : {} + + // Create rigidbody as a parent + const rigidbody = entity.createNode('rigidbody', { + type: rbOpts.type || 'dynamic', + mass: rbOpts.mass || 1, + linearDamping: rbOpts.linearDamping || 0, + angularDamping: rbOpts.angularDamping || 0.05, + tag: rbOpts.tag, + onContactStart: rbOpts.onContactStart, + onContactEnd: rbOpts.onContactEnd, + onTriggerEnter: rbOpts.onTriggerEnter, + onTriggerLeave: rbOpts.onTriggerLeave + }) + + // Create matching collider + const colliderOpts = { + type: opts.type, // Use same type as mesh + trigger: rbOpts.trigger || false, + convex: rbOpts.convex || false, + layer: rbOpts.layer || 'prop' + } + + // Set dimensions based on primitive type + if (opts.type === 'box') { + colliderOpts.width = opts.width || 1 + colliderOpts.height = opts.height || 1 + colliderOpts.depth = opts.depth || 1 + } else if (opts.type === 'sphere') { + colliderOpts.radius = opts.radius || 0.5 + } else if (opts.type === 'cylinder' || opts.type === 'cone') { + // For cylinder/cone, use box approximation or custom geometry + // Box approximation based on dimensions + colliderOpts.type = 'box' + colliderOpts.width = (opts.radiusBottom || opts.radius || 0.5) * 2 + colliderOpts.height = opts.height || 1 + colliderOpts.depth = (opts.radiusBottom || opts.radius || 0.5) * 2 + } + + const collider = entity.createNode('collider', colliderOpts) + rigidbody.add(collider) + + // Copy mesh position to rigidbody and clear mesh position + rigidbody.position.copy(node.position) + rigidbody.rotation.copy(node.rotation) + rigidbody.scale.copy(node.scale) + node.position.set(0, 0, 0) + node.rotation.set(0, 0, 0) + node.scale.set(1, 1, 1) + + // Add mesh as child of rigidbody + rigidbody.add(node) + self.worldMethods.add(entity, rigidbody) + + // Get proxies + const rbProxy = rigidbody.getProxy() + const meshProxy = node.getProxy() + + // Add material property that forwards to the mesh child + Object.defineProperty(rbProxy, 'material', { + get() { + // Return the mesh proxy's material + return meshProxy.material + }, + configurable: true + }) + + // Return the enhanced rigidbody proxy + return rbProxy + } + + return node.getProxy() + }, + box(entity, dims = {}) { + return self.worldMethods.spawnMesh(entity, { type: 'box', ...dims }) + }, + sphere(entity, dims = {}) { + return self.worldMethods.spawnMesh(entity, { type: 'sphere', ...dims }) + }, + cylinder(entity, dims = {}) { + return self.worldMethods.spawnMesh(entity, { type: 'cylinder', ...dims }) + }, + cone(entity, dims = {}) { + return self.worldMethods.spawnMesh(entity, { type: 'cone', ...dims }) + }, } } diff --git a/test-primitives.js b/test-primitives.js new file mode 100644 index 00000000..39294163 --- /dev/null +++ b/test-primitives.js @@ -0,0 +1,65 @@ +// Test script for primitive spawning API +// This demonstrates how to use the new primitive spawning functions + +export default { + init() { + // Create a red box + const box = world.box({ width: 1, height: 1, depth: 1 }) + box.position.set(0, 0.5, -2) + box.material.color = 'red' + + // Create a blue sphere + const sphere = world.sphere({ radius: 0.5 }) + sphere.position.set(2, 0.5, -2) + sphere.material.color = 'blue' + + // Create a green cylinder + const cylinder = world.cylinder({ + radiusTop: 0.3, + radiusBottom: 0.5, + height: 1.5, + radialSegments: 8 + }) + cylinder.position.set(-2, 0.75, -2) + cylinder.material.color = 'green' + + // Create a yellow cone + const cone = world.cone({ + radius: 0.5, + height: 1, + radialSegments: 8 + }) + cone.position.set(0, 0.5, -4) + cone.material.color = 'yellow' + + // Example using the generic spawnMesh method + const customSphere = world.spawnMesh({ type: 'sphere', radius: 0.3 }) + customSphere.position.set(4, 0.3, -2) + customSphere.material.color = '#ff00ff' + + // Store references for later manipulation + this.primitives = { box, sphere, cylinder, cone, customSphere } + }, + + update() { + // Rotate all primitives + const time = world.getTime() * 0.001 + + if (this.primitives) { + this.primitives.box.rotation.y = time + this.primitives.sphere.rotation.x = time + this.primitives.cylinder.rotation.z = time * 0.5 + this.primitives.cone.rotation.x = time * 0.7 + this.primitives.customSphere.position.y = 0.3 + Math.sin(time * 2) * 0.1 + } + }, + + destroy() { + // Clean up - remove all created primitives + if (this.primitives) { + Object.values(this.primitives).forEach(primitive => { + world.remove(primitive) + }) + } + } +} \ No newline at end of file diff --git a/visible-primitive-test.js b/visible-primitive-test.js new file mode 100644 index 00000000..306f4261 --- /dev/null +++ b/visible-primitive-test.js @@ -0,0 +1,81 @@ +// Test script with better visibility settings + +export default { + init() { + console.log('Creating primitives...') + + // Create multiple primitives with different colors and positions + const primitives = [] + + // Red box + const box = world.box({ width: 1, height: 1, depth: 1 }) + box.position.set(-3, 1, -5) + box.material.color = '#ff0000' + primitives.push(box) + console.log('Created red box at', box.position.toArray()) + + // Blue sphere + const sphere = world.sphere({ radius: 0.5 }) + sphere.position.set(-1, 1, -5) + sphere.material.color = '#0000ff' + primitives.push(sphere) + console.log('Created blue sphere at', sphere.position.toArray()) + + // Green cylinder + const cylinder = world.cylinder({ + radiusTop: 0.3, + radiusBottom: 0.5, + height: 1.5 + }) + cylinder.position.set(1, 1, -5) + cylinder.material.color = '#00ff00' + primitives.push(cylinder) + console.log('Created green cylinder at', cylinder.position.toArray()) + + // Yellow cone + const cone = world.cone({ + radius: 0.5, + height: 1 + }) + cone.position.set(3, 1, -5) + cone.material.color = '#ffff00' + primitives.push(cone) + console.log('Created yellow cone at', cone.position.toArray()) + + // Store references + this.primitives = primitives + + // Debug info + console.log('All primitives created. Total:', primitives.length) + + // Try to make materials more visible with emissive + primitives.forEach((prim, i) => { + if (prim.material.emissiveIntensity !== undefined) { + prim.material.emissiveIntensity = 0.3 + } + console.log(`Primitive ${i} material properties:`, { + color: prim.material.color, + emissiveIntensity: prim.material.emissiveIntensity + }) + }) + }, + + update() { + // Slowly rotate primitives to confirm they exist + if (this.primitives) { + const time = world.getTime() * 0.001 + this.primitives.forEach((prim, i) => { + prim.rotation.y = time + (i * Math.PI / 2) + }) + } + }, + + destroy() { + console.log('Cleaning up primitives...') + if (this.primitives) { + this.primitives.forEach(prim => { + world.remove(prim) + }) + } + } +} \ No newline at end of file diff --git a/working-primitive-example.js b/working-primitive-example.js new file mode 100644 index 00000000..5e4c9fc2 --- /dev/null +++ b/working-primitive-example.js @@ -0,0 +1,29 @@ +// Working example for primitive spawning +// Make sure this is used as the script content for an app + +export default { + init() { + // Create a box positioned above ground + const box = world.box({ width: 1, height: 1, depth: 1 }) + box.position.set(0, 1, -3) // y=1 to be above ground, z=-3 to be in front + box.material.color = '#0000ff' // Try hex color + + // Also try setting emissive for visibility + if (box.material.emissive) { + box.material.emissive = '#000044' + box.material.emissiveIntensity = 0.2 + } + + console.log('Box created at:', box.position.x, box.position.y, box.position.z) + console.log('Box material:', box.material) + + // Store reference for cleanup + this.box = box + }, + + destroy() { + if (this.box) { + world.remove(this.box) + } + } +} \ No newline at end of file From f77bdcb0c655d626cad4f2d6d43537900f37fc22 Mon Sep 17 00:00:00 2001 From: saori-eth Date: Sat, 21 Jun 2025 18:55:13 -0500 Subject: [PATCH 2/2] clean --- apps/syncd-primitive-ball.js | 178 +++++++++++++++++++++++++++++++++++ docs/{ => ref}/primitives.md | 0 docs/scripts.md | 64 +------------ example-primitive-app.js | 47 --------- physics-primitive-test.js | 116 ----------------------- primitive-app-setup.md | 68 ------------- test-primitives.js | 65 ------------- visible-primitive-test.js | 81 ---------------- working-primitive-example.js | 29 ------ 9 files changed, 179 insertions(+), 469 deletions(-) create mode 100644 apps/syncd-primitive-ball.js rename docs/{ => ref}/primitives.md (100%) delete mode 100644 example-primitive-app.js delete mode 100644 physics-primitive-test.js delete mode 100644 primitive-app-setup.md delete mode 100644 test-primitives.js delete mode 100644 visible-primitive-test.js delete mode 100644 working-primitive-example.js diff --git a/apps/syncd-primitive-ball.js b/apps/syncd-primitive-ball.js new file mode 100644 index 00000000..09e12de3 --- /dev/null +++ b/apps/syncd-primitive-ball.js @@ -0,0 +1,178 @@ +// Networked primitive with ownership model +// Similar to the tumbleweed example - any player can grab control + +const SEND_RATE = 1 / 8 +const v1 = new Vector3() + +// Create a physics-enabled primitive +const ball = world.sphere({ + radius: 0.5, + rigidbody: { + type: 'dynamic', + mass: 0.5, + linearDamping: 0.1, + angularDamping: 0.5, + onContactStart: (e) => { + // When a player touches the ball, they take ownership + if (e.playerId && e.playerId === world.networkId) { + ownerId = world.networkId + app.send('take', ownerId) + } + } + } +}) + +// Position it somewhere visible +ball.position.set(0, 2, -5) +ball.material.color = '#ff00ff' +ball.material.emissiveIntensity = 0.3 + +let ownerId = null +let lastSent = 0 + +if (world.isClient) { + // Wait for initial state from server + if (app.state.ready) { + init(app.state) + } else { + app.on('state', init) + } + + function init(state) { + // Apply initial state + ball.position.fromArray(state.p) + ball.quaternion.fromArray(state.q) + ball.setLinearVelocity(v1.fromArray(state.v)) + + // Set up interpolation for smooth movement + const npos = new LerpVector3(ball.position, SEND_RATE) + const nqua = new LerpQuaternion(ball.quaternion, SEND_RATE) + + // Handle ownership changes + app.on('take', (newOwnerId) => { + if (ownerId === newOwnerId) return + ownerId = newOwnerId + npos.snap() + nqua.snap() + + // Visual feedback for ownership + if (ownerId === world.networkId) { + ball.material.color = '#00ff00' // Green when you own it + } else if (ownerId) { + ball.material.color = '#ff0000' // Red when someone else owns it + } else { + ball.material.color = '#ff00ff' // Purple when no owner + } + }) + + // Receive position updates + app.on('move', (e) => { + if (ownerId === world.networkId) return // Ignore if we own it + npos.pushArray(e.p) + nqua.pushArray(e.q) + ball.setLinearVelocity(v1.fromArray(e.v)) + }) + + // Update loop + app.on('update', (delta) => { + if (ball.sleeping) return + + if (ownerId === world.networkId) { + // We own it - send updates + lastSent += delta + if (lastSent > SEND_RATE) { + lastSent = 0 + app.send('move', { + p: ball.position.toArray(), + q: ball.quaternion.toArray(), + v: ball.getLinearVelocity(v1).toArray(), + }) + } + } else { + // Someone else owns it - interpolate + npos.update(delta) + nqua.update(delta) + } + }) + } + + // Allow kicking the ball when you own it + world.on('keydown', (event) => { + if (event.key === ' ' && ownerId === world.networkId) { + const force = new Vector3( + (Math.random() - 0.5) * 10, + 10, + (Math.random() - 0.5) * 10 + ) + ball.addForce(force, 'impulse') + } + }) +} + +if (world.isServer) { + // Initialize state + app.state.ready = true + app.state.p = ball.position.toArray() + app.state.q = ball.quaternion.toArray() + app.state.v = [0, 0, 0] + app.send('state', app.state) + + // Handle ownership changes + app.on('take', (newOwnerId, networkId) => { + ownerId = newOwnerId + app.send('take', newOwnerId) // Broadcast to all clients + }) + + // Relay position updates + app.on('move', (e, networkId) => { + // Only accept moves from the current owner + if (networkId !== ownerId) return + + app.state.p = e.p + app.state.q = e.q + app.state.v = e.v + ball.position.fromArray(e.p) + ball.quaternion.fromArray(e.q) + ball.setLinearVelocity(v1.fromArray(e.v)) + app.send('move', e, networkId) // Send to all except sender + }) + + // Handle player disconnection + world.on('leave', (e) => { + if (e.player.networkId === ownerId) { + ownerId = null + app.send('take', null) + } + }) + + // Server takes control when no one owns it + app.on('update', (delta) => { + if (ball.sleeping) return + + if (!ownerId) { + lastSent += delta + if (lastSent > SEND_RATE) { + app.state.p = ball.position.toArray() + app.state.q = ball.quaternion.toArray() + app.state.v = ball.getLinearVelocity(v1).toArray() + app.send('move', { + p: app.state.p, + q: app.state.q, + v: app.state.v, + }) + lastSent = 0 + } + } + }) +} + +// Cleanup +app.on('destroy', () => { + app.off('state') + app.off('take') + app.off('move') + app.off('update') + world.off('keydown') + world.off('leave') + world.remove(ball) +}) \ No newline at end of file diff --git a/docs/primitives.md b/docs/ref/primitives.md similarity index 100% rename from docs/primitives.md rename to docs/ref/primitives.md diff --git a/docs/scripts.md b/docs/scripts.md index 7c144090..f6c9752c 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -42,66 +42,4 @@ Some nodes can also be created and used on the fly using `app.create(nodeName)`. - [Controller](/docs/ref/Controller.md) - [RigidBody](/docs/ref/RigidBody.md) - [Collider](/docs/ref/Collider.md) -- [Joint](/docs/ref/Joint.md) - -## Creating Primitives - -Hyperfy provides convenient methods to create primitive meshes directly from scripts without needing to load external models. - -### Basic Usage - -```js -// Create a box -const cube = world.box({ width: 1, height: 1, depth: 1 }) -cube.position.set(0, 0.5, -2) -cube.material.color = 'red' - -// Create a sphere -const sphere = world.sphere({ radius: 0.5 }) -sphere.position.set(2, 0.5, -2) -sphere.material.color = 'blue' - -// Create a cylinder -const cylinder = world.cylinder({ - radiusTop: 0.3, - radiusBottom: 0.5, - height: 1.5, - radialSegments: 8 -}) -cylinder.position.set(-2, 0.75, -2) -cylinder.material.color = 'green' - -// Create a cone -const cone = world.cone({ - radius: 0.5, - height: 1, - radialSegments: 8 -}) -cone.position.set(0, 0.5, -4) -cone.material.color = 'yellow' -``` - -### Generic Method - -You can also use the generic `spawnMesh` method: - -```js -const mesh = world.spawnMesh({ - type: 'sphere', // 'box', 'sphere', 'cylinder', or 'cone' - radius: 0.5 // plus any type-specific parameters -}) -``` - -### Parameters - -- **Box**: `width`, `height`, `depth` -- **Sphere**: `radius` -- **Cylinder**: `radiusTop`, `radiusBottom`, `height`, `radialSegments` -- **Cone**: `radius`, `height`, `radialSegments` - -### Notes - -- Meshes created this way are lightweight instanced objects -- They obey normal physics and ray-cast rules -- They use the default unlit material which can be modified via the material proxy -- Remember to remove primitives when no longer needed: `world.remove(mesh)` \ No newline at end of file +- [Joint](/docs/ref/Joint.md) \ No newline at end of file diff --git a/example-primitive-app.js b/example-primitive-app.js deleted file mode 100644 index d3454faf..00000000 --- a/example-primitive-app.js +++ /dev/null @@ -1,47 +0,0 @@ -// Example app script that uses primitive spawning -// This should be used as the script content for an app, not as the model URL - -export default { - init() { - // Create primitives when the app initializes - this.primitives = [] - - // Create a red box - const box = world.box({ width: 1, height: 1, depth: 1 }) - box.position.set(0, 0.5, 0) - box.material.color = 'red' - this.primitives.push(box) - - // Create a blue sphere - const sphere = world.sphere({ radius: 0.5 }) - sphere.position.set(2, 0.5, 0) - sphere.material.color = 'blue' - this.primitives.push(sphere) - - // Create a green cylinder - const cylinder = world.cylinder({ - radiusTop: 0.3, - radiusBottom: 0.5, - height: 1.5, - radialSegments: 8 - }) - cylinder.position.set(-2, 0.75, 0) - cylinder.material.color = 'green' - this.primitives.push(cylinder) - }, - - update() { - // Rotate primitives - const time = world.getTime() * 0.001 - this.primitives.forEach((primitive, i) => { - primitive.rotation.y = time + i - }) - }, - - destroy() { - // Clean up primitives when app is destroyed - this.primitives.forEach(primitive => { - world.remove(primitive) - }) - } -} \ No newline at end of file diff --git a/physics-primitive-test.js b/physics-primitive-test.js deleted file mode 100644 index b0bd9fb3..00000000 --- a/physics-primitive-test.js +++ /dev/null @@ -1,116 +0,0 @@ -// Test script for primitives with physics - -export default { - init() { - console.log('Creating physics primitives...') - - this.primitives = [] - - // Create a static floor - const floor = world.box({ - width: 20, - height: 0.1, - depth: 20, - rigidbody: { - type: 'static' - } - }) - floor.position.set(0, 0, -5) - floor.children[0].material.color = '#444444' - this.primitives.push(floor) - - // Create falling boxes with different masses - for (let i = 0; i < 3; i++) { - const box = world.box({ - width: 1, - height: 1, - depth: 1, - rigidbody: { - type: 'dynamic', - mass: 0.5 + i * 0.5, // 0.5, 1.0, 1.5 kg - linearDamping: 0.1, - angularDamping: 0.5 - } - }) - box.position.set(-2 + i * 2, 3 + i, -5) - box.children[0].material.color = `hsl(${i * 120}, 100%, 50%)` - this.primitives.push(box) - } - - // Create a bouncy ball - const ball = world.sphere({ - radius: 0.5, - rigidbody: { - type: 'dynamic', - mass: 0.3, - layer: 'prop', - tag: 'ball' - } - }) - ball.position.set(0, 5, -5) - ball.children[0].material.color = '#ff00ff' - ball.setLinearVelocity(new Vector3(2, 0, 0)) - this.primitives.push(ball) - - // Create a trigger zone - const trigger = world.box({ - width: 3, - height: 3, - depth: 3, - rigidbody: { - type: 'static', - trigger: true, - onTriggerEnter: (other) => { - console.log('Something entered trigger:', other.tag) - trigger.children[0].material.color = '#00ff00' - }, - onTriggerLeave: (other) => { - console.log('Something left trigger:', other.tag) - trigger.children[0].material.color = '#ff000080' - } - } - }) - trigger.position.set(5, 1.5, -5) - trigger.children[0].material.color = '#ff000080' - trigger.children[0].material.emissiveIntensity = 0.5 - this.primitives.push(trigger) - - // Create a kinematic platform - const platform = world.box({ - width: 3, - height: 0.5, - depth: 2, - rigidbody: { - type: 'kinematic' - } - }) - platform.position.set(-5, 2, -5) - platform.children[0].material.color = '#00ff00' - this.primitives.push(platform) - this.platform = platform - - console.log('Created', this.primitives.length, 'physics primitives') - }, - - update() { - // Animate the kinematic platform - if (this.platform) { - const time = world.getTime() * 0.001 - const newPos = new Vector3( - -5 + Math.sin(time) * 3, - 2 + Math.sin(time * 2) * 0.5, - -5 - ) - this.platform.setKinematicTarget(newPos, this.platform.quaternion) - } - }, - - destroy() { - console.log('Cleaning up physics primitives...') - if (this.primitives) { - this.primitives.forEach(prim => { - world.remove(prim) - }) - } - } -} \ No newline at end of file diff --git a/primitive-app-setup.md b/primitive-app-setup.md deleted file mode 100644 index 707be96b..00000000 --- a/primitive-app-setup.md +++ /dev/null @@ -1,68 +0,0 @@ -# How to Use Primitive Spawning in Hyperfy Apps - -## Important: Understanding the Architecture - -Apps in Hyperfy typically have: -1. A **model** (GLB file) - The 3D content -2. A **script** (JS file) - The behavior/logic - -For primitive spawning, you don't need a model file since primitives are created dynamically by the script. - -## Method 1: Create an App with Empty/Minimal Model - -1. Create a minimal GLB file (can be an empty scene or a single invisible object) -2. Create your script that spawns primitives -3. Drop the GLB file into the world to create the app -4. Add your script to the app - -## Method 2: Use Console for Testing - -You can test primitive spawning directly in the browser console: - -```js -// Access the world object -const w = window.world - -// Create a box -const box = w.box({ width: 1, height: 1, depth: 1 }) -box.position.set(0, 1, -5) -box.material.color = 'red' - -// Create a sphere -const sphere = w.sphere({ radius: 0.5 }) -sphere.position.set(2, 1, -5) -sphere.material.color = 'blue' - -// Remove them later -w.remove(box) -w.remove(sphere) -``` - -## Method 3: Modify an Existing App - -1. Select an existing app in your world -2. Open the script editor -3. Replace or modify the script to include primitive spawning: - -```js -export default { - init() { - // Your existing code... - - // Add primitive spawning - const cube = world.box({ width: 0.5, height: 0.5, depth: 0.5 }) - cube.position.set(0, 1, 0) - cube.material.color = 'yellow' - } -} -``` - -## Common Issues - -1. **"primitive" is not a valid model URL** - Don't set the app's model to "primitive", use a real GLB file -2. **Primitives not showing** - Make sure to set position, primitives spawn at origin (0,0,0) by default -3. **Memory leaks** - Always remove primitives in the `destroy()` method - -## Example Script - -See `example-primitive-app.js` for a complete working example that demonstrates all primitive types with animation. \ No newline at end of file diff --git a/test-primitives.js b/test-primitives.js deleted file mode 100644 index 39294163..00000000 --- a/test-primitives.js +++ /dev/null @@ -1,65 +0,0 @@ -// Test script for primitive spawning API -// This demonstrates how to use the new primitive spawning functions - -export default { - init() { - // Create a red box - const box = world.box({ width: 1, height: 1, depth: 1 }) - box.position.set(0, 0.5, -2) - box.material.color = 'red' - - // Create a blue sphere - const sphere = world.sphere({ radius: 0.5 }) - sphere.position.set(2, 0.5, -2) - sphere.material.color = 'blue' - - // Create a green cylinder - const cylinder = world.cylinder({ - radiusTop: 0.3, - radiusBottom: 0.5, - height: 1.5, - radialSegments: 8 - }) - cylinder.position.set(-2, 0.75, -2) - cylinder.material.color = 'green' - - // Create a yellow cone - const cone = world.cone({ - radius: 0.5, - height: 1, - radialSegments: 8 - }) - cone.position.set(0, 0.5, -4) - cone.material.color = 'yellow' - - // Example using the generic spawnMesh method - const customSphere = world.spawnMesh({ type: 'sphere', radius: 0.3 }) - customSphere.position.set(4, 0.3, -2) - customSphere.material.color = '#ff00ff' - - // Store references for later manipulation - this.primitives = { box, sphere, cylinder, cone, customSphere } - }, - - update() { - // Rotate all primitives - const time = world.getTime() * 0.001 - - if (this.primitives) { - this.primitives.box.rotation.y = time - this.primitives.sphere.rotation.x = time - this.primitives.cylinder.rotation.z = time * 0.5 - this.primitives.cone.rotation.x = time * 0.7 - this.primitives.customSphere.position.y = 0.3 + Math.sin(time * 2) * 0.1 - } - }, - - destroy() { - // Clean up - remove all created primitives - if (this.primitives) { - Object.values(this.primitives).forEach(primitive => { - world.remove(primitive) - }) - } - } -} \ No newline at end of file diff --git a/visible-primitive-test.js b/visible-primitive-test.js deleted file mode 100644 index 306f4261..00000000 --- a/visible-primitive-test.js +++ /dev/null @@ -1,81 +0,0 @@ -// Test script with better visibility settings - -export default { - init() { - console.log('Creating primitives...') - - // Create multiple primitives with different colors and positions - const primitives = [] - - // Red box - const box = world.box({ width: 1, height: 1, depth: 1 }) - box.position.set(-3, 1, -5) - box.material.color = '#ff0000' - primitives.push(box) - console.log('Created red box at', box.position.toArray()) - - // Blue sphere - const sphere = world.sphere({ radius: 0.5 }) - sphere.position.set(-1, 1, -5) - sphere.material.color = '#0000ff' - primitives.push(sphere) - console.log('Created blue sphere at', sphere.position.toArray()) - - // Green cylinder - const cylinder = world.cylinder({ - radiusTop: 0.3, - radiusBottom: 0.5, - height: 1.5 - }) - cylinder.position.set(1, 1, -5) - cylinder.material.color = '#00ff00' - primitives.push(cylinder) - console.log('Created green cylinder at', cylinder.position.toArray()) - - // Yellow cone - const cone = world.cone({ - radius: 0.5, - height: 1 - }) - cone.position.set(3, 1, -5) - cone.material.color = '#ffff00' - primitives.push(cone) - console.log('Created yellow cone at', cone.position.toArray()) - - // Store references - this.primitives = primitives - - // Debug info - console.log('All primitives created. Total:', primitives.length) - - // Try to make materials more visible with emissive - primitives.forEach((prim, i) => { - if (prim.material.emissiveIntensity !== undefined) { - prim.material.emissiveIntensity = 0.3 - } - console.log(`Primitive ${i} material properties:`, { - color: prim.material.color, - emissiveIntensity: prim.material.emissiveIntensity - }) - }) - }, - - update() { - // Slowly rotate primitives to confirm they exist - if (this.primitives) { - const time = world.getTime() * 0.001 - this.primitives.forEach((prim, i) => { - prim.rotation.y = time + (i * Math.PI / 2) - }) - } - }, - - destroy() { - console.log('Cleaning up primitives...') - if (this.primitives) { - this.primitives.forEach(prim => { - world.remove(prim) - }) - } - } -} \ No newline at end of file diff --git a/working-primitive-example.js b/working-primitive-example.js deleted file mode 100644 index 5e4c9fc2..00000000 --- a/working-primitive-example.js +++ /dev/null @@ -1,29 +0,0 @@ -// Working example for primitive spawning -// Make sure this is used as the script content for an app - -export default { - init() { - // Create a box positioned above ground - const box = world.box({ width: 1, height: 1, depth: 1 }) - box.position.set(0, 1, -3) // y=1 to be above ground, z=-3 to be in front - box.material.color = '#0000ff' // Try hex color - - // Also try setting emissive for visibility - if (box.material.emissive) { - box.material.emissive = '#000044' - box.material.emissiveIntensity = 0.2 - } - - console.log('Box created at:', box.position.x, box.position.y, box.position.z) - console.log('Box material:', box.material) - - // Store reference for cleanup - this.box = box - }, - - destroy() { - if (this.box) { - world.remove(this.box) - } - } -} \ No newline at end of file