From 9df64b7dd956c9e351818b2af149afdf5287c53f Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Thu, 30 Oct 2025 09:37:30 -0500 Subject: [PATCH] Add background starfield and fix scene background color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created BackgroundStars class using PointCloudSystem: - 5000 stars distributed uniformly on sphere surface - Multiple star colors (white, warm, cool, yellowish, bluish) - Varied brightness (0.3-1.0) for depth perception - Follows camera position to maintain infinite distance effect - Efficient rendering with disabled lighting and depth write Integrated starfield into Level1: - Created during level initialization - Camera follow in render loop - Proper disposal on level cleanup Fixed XR background color: - Set scene clearColor to pure black (was default grey) - Adjusted ambientColor to black for space environment Removed GlowLayer from ship and engines: - Cleaned up unused glow effects - Prevents unwanted glow on background stars 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/backgroundStars.ts | 155 +++++++++++++++++++++++++++++++++++++++++ src/level1.ts | 22 ++++++ src/main.ts | 6 +- src/ship.ts | 4 +- src/shipEngine.ts | 7 +- 5 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 src/backgroundStars.ts diff --git a/src/backgroundStars.ts b/src/backgroundStars.ts new file mode 100644 index 0000000..fa9a825 --- /dev/null +++ b/src/backgroundStars.ts @@ -0,0 +1,155 @@ +import {Color3, Color4, PointsCloudSystem, Scene, Vector3} from "@babylonjs/core"; + +/** + * Configuration options for background stars + */ +export interface BackgroundStarsConfig { + /** Number of stars to generate */ + count?: number; + /** Radius of the sphere containing the stars */ + radius?: number; + /** Minimum star brightness (0-1) */ + minBrightness?: number; + /** Maximum star brightness (0-1) */ + maxBrightness?: number; + /** Star point size */ + pointSize?: number; + /** Star colors (will be randomly selected) */ + colors?: Color4[]; +} + +/** + * Generates a spherical field of background stars using PointCloudSystem + */ +export class BackgroundStars { + private pcs: PointsCloudSystem; + private scene: Scene; + private config: Required; + + // Default configuration + private static readonly DEFAULT_CONFIG: Required = { + count: 5000, + radius: 5000, + minBrightness: 0.3, + maxBrightness: 1.0, + pointSize: .1, + colors: [ + new Color4(1, 1, 1, 1), // White + new Color4(1, 0.95, 0.9, 1), // Warm white + new Color4(0.9, 0.95, 1, 1), // Cool white + new Color4(1, 0.9, 0.8, 1), // Yellowish + new Color4(0.8, 0.9, 1, 1) // Bluish + ] + }; + + constructor(scene: Scene, config?: BackgroundStarsConfig) { + this.scene = scene; + this.config = { ...BackgroundStars.DEFAULT_CONFIG, ...config }; + this.createStarfield(); + } + + /** + * Create the starfield using PointCloudSystem + */ + private createStarfield(): void { + // Create point cloud system + this.pcs = new PointsCloudSystem("backgroundStars", this.config.pointSize, this.scene); + + // Function to set position and color for each particle + const initParticle = (particle: any) => { + // Generate random position on sphere surface with some depth variation + const theta = Math.random() * Math.PI * 2; // Azimuth angle (0 to 2π) + const phi = Math.acos(2 * Math.random() - 1); // Polar angle (0 to π) - uniform distribution + + // Add some randomness to radius for depth + const radiusVariation = this.config.radius * (0.8 + Math.random() * 0.2); + + // Convert spherical coordinates to Cartesian + particle.position = new Vector3( + radiusVariation * Math.sin(phi) * Math.cos(theta), + radiusVariation * Math.sin(phi) * Math.sin(theta), + radiusVariation * Math.cos(phi) + ); + + // Random brightness + const brightness = this.config.minBrightness + + Math.random() * (this.config.maxBrightness - this.config.minBrightness); + + // Random color from palette + const baseColor = this.config.colors[Math.floor(Math.random() * this.config.colors.length)]; + + // Apply brightness to color + particle.color = new Color4( + baseColor.r * brightness, + baseColor.g * brightness, + baseColor.b * brightness, + 1 + ); + }; + + // Add particles to the system + this.pcs.addPoints(this.config.count, initParticle); + + // Build the mesh + this.pcs.buildMeshAsync().then(() => { + const mesh = this.pcs.mesh; + if (mesh) { + // Stars should not receive lighting + mesh.material.disableLighting = true; + mesh.material.emissiveColor = new Color3(1,1,1); + + // Disable depth write so stars don't occlude other objects + mesh.material.disableDepthWrite = true; + + // Stars should be in the background + mesh.renderingGroupId = 0; + + // Make stars always render behind everything else + mesh.isPickable = false; + + console.log(`Created ${this.config.count} background stars`); + } + }); + } + + /** + * Update star positions to follow camera (keeps stars at infinite distance) + */ + public followCamera(cameraPosition: Vector3): void { + if (this.pcs.mesh) { + this.pcs.mesh.position = cameraPosition; + } + } + + /** + * Dispose of the starfield + */ + public dispose(): void { + if (this.pcs) { + this.pcs.dispose(); + } + } + + /** + * Get the point cloud system + */ + public getPointCloudSystem(): PointsCloudSystem { + return this.pcs; + } + + /** + * Get the mesh + */ + public getMesh() { + return this.pcs?.mesh; + } + + /** + * Set the visibility of the stars + */ + public setVisible(visible: boolean): void { + if (this.pcs?.mesh) { + this.pcs.mesh.isVisible = visible; + } + } +} diff --git a/src/level1.ts b/src/level1.ts index 4d85dbf..4a105c1 100644 --- a/src/level1.ts +++ b/src/level1.ts @@ -20,6 +20,7 @@ import {Scoreboard} from "./scoreboard"; import setLoadingMessage from "./setLoadingMessage"; import {LevelConfig} from "./levelConfig"; import {LevelDeserializer} from "./levelDeserializer"; +import {BackgroundStars} from "./backgroundStars"; export class Level1 implements Level { private _ship: Ship; @@ -31,6 +32,7 @@ export class Level1 implements Level { private _levelConfig: LevelConfig; private _audioEngine: AudioEngineV2; private _deserializer: LevelDeserializer; + private _backgroundStars: BackgroundStars; constructor(levelConfig: LevelConfig, audioEngine: AudioEngineV2) { this._levelConfig = levelConfig; @@ -94,6 +96,9 @@ export class Level1 implements Level { public dispose() { this._startBase.dispose(); this._endBase.dispose(); + if (this._backgroundStars) { + this._backgroundStars.dispose(); + } } public async initialize() { console.log('Initializing level from config:', this._levelConfig.difficulty); @@ -132,6 +137,23 @@ export class Level1 implements Level { } } + // Create background starfield + setLoadingMessage("Creating starfield..."); + this._backgroundStars = new BackgroundStars(DefaultScene.MainScene, { + count: 5000, + radius: 5000, + minBrightness: 0.3, + maxBrightness: 1.0, + pointSize: 2 + }); + + // Set up camera follow for stars (keeps stars at infinite distance) + DefaultScene.MainScene.onBeforeRenderObservable.add(() => { + if (this._backgroundStars && DefaultScene.XR.baseExperience.camera) { + this._backgroundStars.followCamera(DefaultScene.XR.baseExperience.camera.position); + } + }); + this._initialized = true; // Notify that initialization is complete diff --git a/src/main.ts b/src/main.ts index 50b3dbc..dfd3a4e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -134,7 +134,9 @@ export class Main { } DefaultScene.DemoScene = new Scene(this._engine); DefaultScene.MainScene = new Scene(this._engine); - DefaultScene.MainScene.ambientColor = new Color3(.5, .5, .5); + DefaultScene.MainScene.ambientColor = new Color3(0,0,0); + DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4(); + setLoadingMessage("Initializing Physics Engine.."); await this.setupPhysics(); @@ -170,7 +172,7 @@ export class Main { private async setupPhysics() { const havok = await HavokPhysics(); const havokPlugin = new HavokPlugin(true, havok); - DefaultScene.MainScene.ambientColor = new Color3(.1, .1, .1); + //DefaultScene.MainScene.ambientColor = new Color3(.1, .1, .1); const light = new DirectionalLight("dirLight", new Vector3(-1, -2, -1), DefaultScene.MainScene); DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin); DefaultScene.MainScene.getPhysicsEngine().setTimeStep(1/30); diff --git a/src/ship.ts b/src/ship.ts index 6a64586..d72976f 100644 --- a/src/ship.ts +++ b/src/ship.ts @@ -58,7 +58,6 @@ export class Ship { private _ammoMaterial: StandardMaterial; private _forwardNode: TransformNode; private _rotationNode: TransformNode; - private _glowLayer: GlowLayer; private _primaryThrustVectorSound: StaticSound; private _secondaryThrustVectorSound: StaticSound; private _shot: StaticSound; @@ -147,8 +146,7 @@ export class Ship { } private setup() { this._ship = new TransformNode("ship", DefaultScene.MainScene); - this._glowLayer = new GlowLayer('bullets', DefaultScene.MainScene); - this._glowLayer.intensity = 1; + // Create sounds asynchronously if audio engine is available if (this._audioEngine) { diff --git a/src/shipEngine.ts b/src/shipEngine.ts index fed0e73..256977f 100644 --- a/src/shipEngine.ts +++ b/src/shipEngine.ts @@ -17,15 +17,14 @@ export class ShipEngine { private _ship: TransformNode; private _leftMainEngine: MainEngine; private _rightMainEngine: MainEngine; - private _gl: GlowLayer; + constructor(ship: TransformNode) { this._ship = ship; this.initialize(); } private initialize() { - this._gl = new GlowLayer("glow", DefaultScene.MainScene); - this._gl.intensity =1; + this._leftMainEngine = this.createEngine(new Vector3(-.44, .37, -1.1)); this._rightMainEngine = this.createEngine(new Vector3(.44, .37, -1.1)); } @@ -52,7 +51,7 @@ export class ShipEngine { engine.parent = this._ship; engine.position = position; const leftDisc = MeshBuilder.CreateIcoSphere("engineSphere", {radius: .07}, DefaultScene.MainScene); - this._gl.addIncludedOnlyMesh(leftDisc); + const material = new StandardMaterial("material", DefaultScene.MainScene); material.emissiveColor = new Color3(.5, .5, .1); leftDisc.material = material;