Add GameConfig system with texture quality levels and physics toggle
Some checks failed
Build / build (push) Failing after 18s

Created GameConfig singleton class with localStorage persistence for game settings:
- Texture quality levels: WIREFRAME, SIMPLE_MATERIAL, FULL_TEXTURE, PBR_TEXTURE
- Physics enable/disable toggle for performance optimization

Created MaterialFactory for quality-level-based material generation:
- Planet materials with dynamic sun-oriented lightmaps
- Asteroid materials preserving GLB bump textures
- Sun materials with procedural fire textures

Integrated GameConfig throughout game entities:
- Conditional physics creation in asteroids, ship, start base
- Material creation respects texture quality settings
- Physics constraints only applied when physics enabled

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-10-30 08:53:11 -05:00
parent 03f170e150
commit 181a427875
6 changed files with 548 additions and 144 deletions

87
src/gameConfig.ts Normal file
View File

@ -0,0 +1,87 @@
/**
* Texture detail levels for game objects
*/
export enum TextureLevel {
WIREFRAME = 'WIREFRAME',
SIMPLE_MATERIAL = 'SIMPLE_MATERIAL',
FULL_TEXTURE = 'FULL_TEXTURE',
PBR_TEXTURE = 'PBR_TEXTURE'
}
/**
* Global game configuration settings
* Singleton class for managing game-wide settings
*/
export class GameConfig {
private static _instance: GameConfig;
// Texture detail settings
public planetTextureLevel: TextureLevel = TextureLevel.FULL_TEXTURE;
public asteroidTextureLevel: TextureLevel = TextureLevel.FULL_TEXTURE;
public sunTextureLevel: TextureLevel = TextureLevel.FULL_TEXTURE;
// Physics settings
public physicsEnabled: boolean = true;
/**
* Private constructor for singleton pattern
*/
private constructor() {
// Load settings from localStorage if available
this.loadFromStorage();
}
/**
* Get the singleton instance
*/
public static getInstance(): GameConfig {
if (!GameConfig._instance) {
GameConfig._instance = new GameConfig();
}
return GameConfig._instance;
}
/**
* Save current configuration to localStorage
*/
public save(): void {
const config = {
planetTextureLevel: this.planetTextureLevel,
asteroidTextureLevel: this.asteroidTextureLevel,
sunTextureLevel: this.sunTextureLevel,
physicsEnabled: this.physicsEnabled
};
localStorage.setItem('game-config', JSON.stringify(config));
}
/**
* Load configuration from localStorage
*/
private loadFromStorage(): void {
try {
const stored = localStorage.getItem('game-config');
if (stored) {
const config = JSON.parse(stored);
this.planetTextureLevel = config.planetTextureLevel ?? TextureLevel.FULL_TEXTURE;
this.asteroidTextureLevel = config.asteroidTextureLevel ?? TextureLevel.FULL_TEXTURE;
this.sunTextureLevel = config.sunTextureLevel ?? TextureLevel.FULL_TEXTURE;
this.physicsEnabled = config.physicsEnabled ?? true;
} else {
this.save();
}
} catch (error) {
console.warn('Failed to load game config from localStorage:', error);
}
}
/**
* Reset to default settings
*/
public reset(): void {
this.planetTextureLevel = TextureLevel.FULL_TEXTURE;
this.asteroidTextureLevel = TextureLevel.FULL_TEXTURE;
this.sunTextureLevel = TextureLevel.FULL_TEXTURE;
this.physicsEnabled = true;
this.save();
}
}

View File

@ -117,9 +117,10 @@ export class Level1 implements Level {
const shipConfig = this._deserializer.getShipConfig(); const shipConfig = this._deserializer.getShipConfig();
this._ship.position = new Vector3(shipConfig.position[0], shipConfig.position[1], shipConfig.position[2]); this._ship.position = new Vector3(shipConfig.position[0], shipConfig.position[1], shipConfig.position[2]);
// Add distance constraints to asteroids // Add distance constraints to asteroids (if physics enabled)
setLoadingMessage("Configuring physics constraints..."); setLoadingMessage("Configuring physics constraints...");
const asteroidMeshes = entities.asteroids; const asteroidMeshes = entities.asteroids;
if (this._startBase.physicsBody) {
for (let i = 0; i < asteroidMeshes.length; i++) { for (let i = 0; i < asteroidMeshes.length; i++) {
const asteroidMesh = asteroidMeshes[i]; const asteroidMesh = asteroidMeshes[i];
if (asteroidMesh.physicsBody) { if (asteroidMesh.physicsBody) {
@ -129,6 +130,7 @@ export class Level1 implements Level {
this._startBase.physicsBody.addConstraint(asteroidMesh.physicsBody, constraint); this._startBase.physicsBody.addConstraint(asteroidMesh.physicsBody, constraint);
} }
} }
}
this._initialized = true; this._initialized = true;

View File

@ -27,6 +27,8 @@ import {
} from "./levelConfig"; } from "./levelConfig";
import { FireProceduralTexture } from "@babylonjs/procedural-textures"; import { FireProceduralTexture } from "@babylonjs/procedural-textures";
import {createSphereLightmap} from "./sphereLightmap"; import {createSphereLightmap} from "./sphereLightmap";
import { GameConfig } from "./gameConfig";
import { MaterialFactory } from "./materialFactory";
/** /**
* Deserializes a LevelConfig JSON object and creates all entities in the scene * Deserializes a LevelConfig JSON object and creates all entities in the scene
@ -92,8 +94,12 @@ export class LevelDeserializer {
} }
mesh.material = material; mesh.material = material;
// Only create physics if enabled in config
const gameConfig = GameConfig.getInstance();
if (gameConfig.physicsEnabled) {
const agg = new PhysicsAggregate(mesh, PhysicsShapeType.CONVEX_HULL, { mass: 0 }, this.scene); const agg = new PhysicsAggregate(mesh, PhysicsShapeType.CONVEX_HULL, { mass: 0 }, this.scene);
agg.body.setMotionType(PhysicsMotionType.ANIMATED); agg.body.setMotionType(PhysicsMotionType.ANIMATED);
}
return mesh; return mesh;
} }
@ -116,11 +122,13 @@ export class LevelDeserializer {
sun.position = this.arrayToVector3(config.position); sun.position = this.arrayToVector3(config.position);
// Create material with procedural fire texture // Create material using GameConfig texture level
const material = new StandardMaterial("sunMaterial", this.scene); const gameConfig = GameConfig.getInstance();
material.emissiveTexture = new FireProceduralTexture("fire", 1024, this.scene); const material = MaterialFactory.createSunMaterial(
material.emissiveColor = new Color3(0.5, 0.5, 0.1); "sunMaterial",
material.disableLighting = true; gameConfig.sunTextureLevel,
this.scene
);
sun.material = material; sun.material = material;
// Create glow layer // Create glow layer
@ -151,33 +159,16 @@ export class LevelDeserializer {
// Calculate direction from planet to sun // Calculate direction from planet to sun
const toSun = sunPosition.subtract(planetPosition).normalize(); const toSun = sunPosition.subtract(planetPosition).normalize();
// Apply texture // Create material using GameConfig texture level
const material = new StandardMaterial(planetConfig.name + "-material", this.scene); const config = GameConfig.getInstance();
const texture = new Texture(planetConfig.texturePath, this.scene); const material = MaterialFactory.createPlanetMaterial(
planetConfig.name + "-material",
// Create lightmap with bright light pointing toward sun planetConfig.texturePath,
const lightmap = createSphereLightmap( config.planetTextureLevel,
planetConfig.name + "-lightmap", this.scene,
256, // texture size toSun
DefaultScene.MainScene,
toSun, // bright light from sun direction
1, // bright intensity
toSun.negate(), // dim light from opposite direction
0.3, // dim intensity
0.3 // ambient
); );
// Apply to material
// Use emissiveTexture (self-lit) instead of diffuseTexture when lighting is disabled
material.emissiveTexture = texture;
material.lightmapTexture = lightmap;
material.useLightmapAsShadowmap = true;
// Disable standard lighting since we're using baked lightmap
material.disableLighting = true;
material.roughness = 1;
material.specularColor = Color3.Black();
planet.material = material; planet.material = material;
planets.push(planet); planets.push(planet);

304
src/materialFactory.ts Normal file
View File

@ -0,0 +1,304 @@
import {
Color3,
DynamicTexture,
NoiseProceduralTexture,
PBRMaterial,
Scene,
StandardMaterial,
Texture,
Vector3
} from "@babylonjs/core";
import { TextureLevel } from "./gameConfig";
import { FireProceduralTexture } from "@babylonjs/procedural-textures";
import { createSphereLightmap } from "./sphereLightmap";
/**
* Factory for creating materials at different quality levels
*/
export class MaterialFactory {
/**
* Create a planet material based on texture level
*/
public static createPlanetMaterial(
name: string,
texturePath: string,
textureLevel: TextureLevel,
scene: Scene,
sunDirection: Vector3
): StandardMaterial | PBRMaterial {
switch (textureLevel) {
case TextureLevel.WIREFRAME:
return this.createWireframeMaterial(name, scene, new Color3(0.5, 0.5, 0.8));
case TextureLevel.SIMPLE_MATERIAL:
return this.createSimplePlanetMaterial(name, scene);
case TextureLevel.FULL_TEXTURE:
return this.createFullTexturePlanetMaterial(name, texturePath, scene, sunDirection);
case TextureLevel.PBR_TEXTURE:
return this.createPBRPlanetMaterial(name, texturePath, scene, sunDirection);
default:
return this.createFullTexturePlanetMaterial(name, texturePath, scene, sunDirection);
}
}
/**
* Create an asteroid material based on texture level
*/
public static createAsteroidMaterial(
name: string,
textureLevel: TextureLevel,
scene: Scene,
originalMaterial?: PBRMaterial
): StandardMaterial | PBRMaterial {
switch (textureLevel) {
case TextureLevel.WIREFRAME:
return this.createWireframeMaterial(name, scene, new Color3(0.5, 0.5, 0.5));
case TextureLevel.SIMPLE_MATERIAL:
return this.createSimpleAsteroidMaterial(name, scene);
case TextureLevel.FULL_TEXTURE:
return this.createFullTextureAsteroidMaterial(name, scene, originalMaterial);
case TextureLevel.PBR_TEXTURE:
return this.createPBRAsteroidMaterial(name, scene, originalMaterial);
default:
return this.createFullTextureAsteroidMaterial(name, scene, originalMaterial);
}
}
/**
* Create a sun material based on texture level
*/
public static createSunMaterial(
name: string,
textureLevel: TextureLevel,
scene: Scene
): StandardMaterial | PBRMaterial {
switch (textureLevel) {
case TextureLevel.WIREFRAME:
return this.createWireframeMaterial(name, scene, new Color3(1, 1, 0));
case TextureLevel.SIMPLE_MATERIAL:
return this.createSimpleSunMaterial(name, scene);
case TextureLevel.FULL_TEXTURE:
return this.createFullTextureSunMaterial(name, scene);
case TextureLevel.PBR_TEXTURE:
return this.createPBRSunMaterial(name, scene);
default:
return this.createFullTextureSunMaterial(name, scene);
}
}
// ========== Private helper methods ==========
/**
* Create wireframe material
*/
private static createWireframeMaterial(
name: string,
scene: Scene,
color: Color3
): StandardMaterial {
const material = new StandardMaterial(name, scene);
material.wireframe = true;
material.emissiveColor = color;
material.disableLighting = true;
return material;
}
/**
* Create simple planet material with solid color
*/
private static createSimplePlanetMaterial(name: string, scene: Scene): StandardMaterial {
const material = new StandardMaterial(name, scene);
material.diffuseColor = new Color3(0.4, 0.6, 0.8);
material.specularColor = Color3.Black();
return material;
}
/**
* Create full texture planet material (current implementation)
*/
private static createFullTexturePlanetMaterial(
name: string,
texturePath: string,
scene: Scene,
sunDirection: Vector3
): StandardMaterial {
const material = new StandardMaterial(name, scene);
const texture = new Texture(texturePath, scene);
// Create lightmap with bright light pointing toward sun
const lightmap = createSphereLightmap(
name + "-lightmap",
256,
scene,
sunDirection,
1,
sunDirection.negate(),
0.3,
0.3
);
material.emissiveTexture = texture;
material.lightmapTexture = lightmap;
material.useLightmapAsShadowmap = true;
material.disableLighting = true;
material.roughness = 1;
material.specularColor = Color3.Black();
return material;
}
/**
* Create PBR planet material
*/
private static createPBRPlanetMaterial(
name: string,
texturePath: string,
scene: Scene,
sunDirection: Vector3
): PBRMaterial {
const material = new PBRMaterial(name, scene);
const texture = new Texture(texturePath, scene);
// Create lightmap with bright light pointing toward sun
const lightmap = createSphereLightmap(
name + "-lightmap",
256,
scene,
sunDirection,
1,
sunDirection.negate(),
0.3,
0.3
);
material.albedoTexture = texture;
material.lightmapTexture = lightmap;
material.useLightmapAsShadowmap = true;
material.roughness = 0.8;
material.metallic = 0;
return material;
}
/**
* Create simple asteroid material with solid color
*/
private static createSimpleAsteroidMaterial(name: string, scene: Scene): StandardMaterial {
const material = new StandardMaterial(name, scene);
material.diffuseColor = new Color3(0.4, 0.4, 0.4);
material.specularColor = Color3.Black();
return material;
}
/**
* Create full texture asteroid material (current implementation)
*/
private static createFullTextureAsteroidMaterial(name: string, scene: Scene, originalMaterial?: PBRMaterial): StandardMaterial {
// If we have the original material from GLB, use it as a base
if (originalMaterial) {
// Clone the original material to preserve bump texture and other properties
const material = originalMaterial.clone(name) as PBRMaterial;
// Create noise texture for color variation
const noiseTexture = new NoiseProceduralTexture(name + "-noise", 256, scene);
noiseTexture.brightness = 0.6;
noiseTexture.octaves = 4;
// Replace only the albedo texture, keeping bump and other textures
material.albedoTexture = noiseTexture;
material.roughness = 1;
return material as any as StandardMaterial;
}
// Fallback if no original material
const material = new StandardMaterial(name, scene);
const noiseTexture = new NoiseProceduralTexture(name + "-noise", 256, scene);
noiseTexture.brightness = 0.6;
noiseTexture.octaves = 4;
material.ambientTexture = noiseTexture;
material.diffuseTexture = noiseTexture;
material.roughness = 1;
return material;
}
/**
* Create PBR asteroid material
*/
private static createPBRAsteroidMaterial(name: string, scene: Scene, originalMaterial?: PBRMaterial): PBRMaterial {
// If we have the original material from GLB, use it as a base
if (originalMaterial) {
// Clone the original material to preserve bump texture and other properties
const material = originalMaterial.clone(name) as PBRMaterial;
// Create noise texture for color variation
const noiseTexture = new NoiseProceduralTexture(name + "-noise", 256, scene);
noiseTexture.brightness = 0.6;
noiseTexture.octaves = 4;
// Replace only the albedo texture, keeping bump and other textures
material.albedoTexture = noiseTexture;
material.roughness = 1;
material.metallic = 0;
return material;
}
// Fallback if no original material
const material = new PBRMaterial(name, scene);
const noiseTexture = new NoiseProceduralTexture(name + "-noise", 256, scene);
noiseTexture.brightness = 0.6;
noiseTexture.octaves = 4;
material.albedoTexture = noiseTexture;
material.roughness = 1;
material.metallic = 0;
return material;
}
/**
* Create simple sun material with solid color
*/
private static createSimpleSunMaterial(name: string, scene: Scene): StandardMaterial {
const material = new StandardMaterial(name, scene);
material.emissiveColor = new Color3(1, 0.9, 0.2);
material.disableLighting = true;
return material;
}
/**
* Create full texture sun material (current implementation)
*/
private static createFullTextureSunMaterial(name: string, scene: Scene): StandardMaterial {
const material = new StandardMaterial(name, scene);
material.emissiveTexture = new FireProceduralTexture("fire", 1024, scene);
material.emissiveColor = new Color3(0.5, 0.5, 0.1);
material.disableLighting = true;
return material;
}
/**
* Create PBR sun material
*/
private static createPBRSunMaterial(name: string, scene: Scene): PBRMaterial {
const material = new PBRMaterial(name, scene);
material.emissiveTexture = new FireProceduralTexture("fire", 1024, scene);
material.emissiveColor = new Color3(0.5, 0.5, 0.1);
material.unlit = true;
return material;
}
}

View File

@ -21,6 +21,7 @@ import {
} from "@babylonjs/core"; } from "@babylonjs/core";
import type {AudioEngineV2, StaticSound} from "@babylonjs/core"; import type {AudioEngineV2, StaticSound} from "@babylonjs/core";
import {DefaultScene} from "./defaultScene"; import {DefaultScene} from "./defaultScene";
import { GameConfig } from "./gameConfig";
const MAX_FORWARD_THRUST = 40; const MAX_FORWARD_THRUST = 40;
const controllerComponents = [ const controllerComponents = [
@ -97,6 +98,12 @@ export class Ship {
} }
private shoot() { private shoot() {
// Only allow shooting if physics is enabled
const config = GameConfig.getInstance();
if (!config.physicsEnabled) {
return;
}
this._shot?.play(); this._shot?.play();
const ammo = new InstancedMesh("ammo", this._ammoBaseMesh as Mesh); const ammo = new InstancedMesh("ammo", this._ammoBaseMesh as Mesh);
ammo.parent = this._ship; ammo.parent = this._ship;
@ -202,7 +209,9 @@ export class Ship {
shipMesh.name = "shipMesh"; shipMesh.name = "shipMesh";
shipMesh.parent = this._ship; shipMesh.parent = this._ship;
// Create physics aggregate based on the loaded mesh // Create physics aggregate based on the loaded mesh (if physics enabled)
const config = GameConfig.getInstance();
if (config.physicsEnabled) {
// Find the actual geometry mesh (usually meshes[1] or a child) // Find the actual geometry mesh (usually meshes[1] or a child)
//const geometryMesh = importMesh.meshes.find(m => m instanceof Mesh && m.getTotalVertices() > 0) as Mesh; //const geometryMesh = importMesh.meshes.find(m => m instanceof Mesh && m.getTotalVertices() > 0) as Mesh;
const geo = shipMesh.getChildMeshes()[0] const geo = shipMesh.getChildMeshes()[0]
@ -235,6 +244,7 @@ export class Ship {
agg.body.setAngularVelocity(new Vector3(0, 0, 0)); agg.body.setAngularVelocity(new Vector3(0, 0, 0));
agg.body.setCollisionCallbackEnabled(true); agg.body.setCollisionCallbackEnabled(true);
} }
}
//shipMesh.rotation.y = Angle.FromDegrees(90).radians(); //shipMesh.rotation.y = Angle.FromDegrees(90).radians();
//shipMesh.rotation.y = Math.PI; //shipMesh.rotation.y = Math.PI;
//shipMesh.position.y = 1; //shipMesh.position.y = 1;

View File

@ -17,6 +17,8 @@ import {DefaultScene} from "./defaultScene";
import {ScoreEvent} from "./scoreboard"; import {ScoreEvent} from "./scoreboard";
import {Debug} from "@babylonjs/core/Legacy/legacy"; import {Debug} from "@babylonjs/core/Legacy/legacy";
import {createSphereLightmap} from "./sphereLightmap"; import {createSphereLightmap} from "./sphereLightmap";
import { GameConfig } from "./gameConfig";
import { MaterialFactory } from "./materialFactory";
let _particleData: any = null; let _particleData: any = null;
export class Rock { export class Rock {
@ -24,8 +26,8 @@ export class Rock {
constructor(mesh: AbstractMesh) { constructor(mesh: AbstractMesh) {
this._rockMesh = mesh; this._rockMesh = mesh;
} }
public get physicsBody(): PhysicsBody { public get physicsBody(): PhysicsBody | null {
return this._rockMesh.physicsBody; return this._rockMesh.physicsBody || null;
} }
public get position(): Vector3 { public get position(): Vector3 {
return this._rockMesh.getAbsolutePosition(); return this._rockMesh.getAbsolutePosition();
@ -35,6 +37,7 @@ export class Rock {
export class RockFactory { export class RockFactory {
private static _rockMesh: AbstractMesh; private static _rockMesh: AbstractMesh;
private static _rockMaterial: PBRMaterial; private static _rockMaterial: PBRMaterial;
private static _originalMaterial: PBRMaterial = null;
private static _explosionPool: ParticleSystemSet[] = []; private static _explosionPool: ParticleSystemSet[] = [];
private static _poolSize: number = 10; private static _poolSize: number = 10;
private static _viewer: PhysicsViewer = null; private static _viewer: PhysicsViewer = null;
@ -64,18 +67,20 @@ export class RockFactory {
//importMesh.meshes[1].dispose(); //importMesh.meshes[1].dispose();
console.log(importMesh.meshes); console.log(importMesh.meshes);
if (!this._rockMaterial) { if (!this._rockMaterial) {
this._rockMaterial = this._rockMesh.material.clone("asteroid") as PBRMaterial; // Clone the original material from GLB to preserve all textures
this._originalMaterial = this._rockMesh.material.clone("asteroid-original") as PBRMaterial;
console.log('Cloned original material from GLB:', this._originalMaterial);
this._rockMaterial.name = 'asteroid-material'; // Create material using GameConfig texture level
this._rockMaterial.id = 'asteroid-material'; const config = GameConfig.getInstance();
const material = (this._rockMaterial as PBRMaterial) this._rockMaterial = MaterialFactory.createAsteroidMaterial(
const noiseTexture = new NoiseProceduralTexture("asteroid-noise", 256, DefaultScene.MainScene); 'asteroid-material',
noiseTexture.brightness = 0.6; // Brighter base color config.asteroidTextureLevel,
noiseTexture.octaves = 4; // More detaila DefaultScene.MainScene,
material.albedoTexture = noiseTexture; this._originalMaterial
material.roughness = 1; ) as PBRMaterial;
this._rockMesh.material = material; this._rockMesh.material = this._rockMaterial;
importMesh.meshes[1].dispose(false, true); importMesh.meshes[1].dispose(false, true);
importMesh.meshes[0].dispose(); importMesh.meshes[0].dispose();
} }
@ -104,6 +109,9 @@ export class RockFactory {
rock.metadata = {type: 'asteroid'}; rock.metadata = {type: 'asteroid'};
rock.setEnabled(true); rock.setEnabled(true);
// Only create physics if enabled in config
const config = GameConfig.getInstance();
if (config.physicsEnabled) {
// PhysicsAggregate will automatically compute sphere size from mesh bounding info // PhysicsAggregate will automatically compute sphere size from mesh bounding info
// The mesh scaling is already applied, so Babylon will create correctly sized physics shape // The mesh scaling is already applied, so Babylon will create correctly sized physics shape
const agg = new PhysicsAggregate(rock, PhysicsShapeType.SPHERE, { const agg = new PhysicsAggregate(rock, PhysicsShapeType.SPHERE, {
@ -182,6 +190,8 @@ export class RockFactory {
}); });
//body.setAngularVelocity(new Vector3(Math.random(), Math.random(), Math.random())); //body.setAngularVelocity(new Vector3(Math.random(), Math.random(), Math.random()));
// body.setLinearVelocity(Vector3.Random(-10, 10)); // body.setLinearVelocity(Vector3.Random(-10, 10));
}
return new Rock(rock); return new Rock(rock);
} }
} }