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();
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...");
const asteroidMeshes = entities.asteroids;
if (this._startBase.physicsBody) {
for (let i = 0; i < asteroidMeshes.length; i++) {
const asteroidMesh = asteroidMeshes[i];
if (asteroidMesh.physicsBody) {
@ -129,6 +130,7 @@ export class Level1 implements Level {
this._startBase.physicsBody.addConstraint(asteroidMesh.physicsBody, constraint);
}
}
}
this._initialized = true;

View File

@ -27,6 +27,8 @@ import {
} from "./levelConfig";
import { FireProceduralTexture } from "@babylonjs/procedural-textures";
import {createSphereLightmap} from "./sphereLightmap";
import { GameConfig } from "./gameConfig";
import { MaterialFactory } from "./materialFactory";
/**
* Deserializes a LevelConfig JSON object and creates all entities in the scene
@ -92,8 +94,12 @@ export class LevelDeserializer {
}
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);
agg.body.setMotionType(PhysicsMotionType.ANIMATED);
}
return mesh;
}
@ -116,11 +122,13 @@ export class LevelDeserializer {
sun.position = this.arrayToVector3(config.position);
// Create material with procedural fire texture
const material = new StandardMaterial("sunMaterial", this.scene);
material.emissiveTexture = new FireProceduralTexture("fire", 1024, this.scene);
material.emissiveColor = new Color3(0.5, 0.5, 0.1);
material.disableLighting = true;
// Create material using GameConfig texture level
const gameConfig = GameConfig.getInstance();
const material = MaterialFactory.createSunMaterial(
"sunMaterial",
gameConfig.sunTextureLevel,
this.scene
);
sun.material = material;
// Create glow layer
@ -151,33 +159,16 @@ export class LevelDeserializer {
// Calculate direction from planet to sun
const toSun = sunPosition.subtract(planetPosition).normalize();
// Apply texture
const material = new StandardMaterial(planetConfig.name + "-material", this.scene);
const texture = new Texture(planetConfig.texturePath, this.scene);
// Create lightmap with bright light pointing toward sun
const lightmap = createSphereLightmap(
planetConfig.name + "-lightmap",
256, // texture size
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
// Create material using GameConfig texture level
const config = GameConfig.getInstance();
const material = MaterialFactory.createPlanetMaterial(
planetConfig.name + "-material",
planetConfig.texturePath,
config.planetTextureLevel,
this.scene,
toSun
);
// 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;
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";
import type {AudioEngineV2, StaticSound} from "@babylonjs/core";
import {DefaultScene} from "./defaultScene";
import { GameConfig } from "./gameConfig";
const MAX_FORWARD_THRUST = 40;
const controllerComponents = [
@ -97,6 +98,12 @@ export class Ship {
}
private shoot() {
// Only allow shooting if physics is enabled
const config = GameConfig.getInstance();
if (!config.physicsEnabled) {
return;
}
this._shot?.play();
const ammo = new InstancedMesh("ammo", this._ammoBaseMesh as Mesh);
ammo.parent = this._ship;
@ -202,7 +209,9 @@ export class Ship {
shipMesh.name = "shipMesh";
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)
//const geometryMesh = importMesh.meshes.find(m => m instanceof Mesh && m.getTotalVertices() > 0) as Mesh;
const geo = shipMesh.getChildMeshes()[0]
@ -235,6 +244,7 @@ export class Ship {
agg.body.setAngularVelocity(new Vector3(0, 0, 0));
agg.body.setCollisionCallbackEnabled(true);
}
}
//shipMesh.rotation.y = Angle.FromDegrees(90).radians();
//shipMesh.rotation.y = Math.PI;
//shipMesh.position.y = 1;

View File

@ -17,6 +17,8 @@ import {DefaultScene} from "./defaultScene";
import {ScoreEvent} from "./scoreboard";
import {Debug} from "@babylonjs/core/Legacy/legacy";
import {createSphereLightmap} from "./sphereLightmap";
import { GameConfig } from "./gameConfig";
import { MaterialFactory } from "./materialFactory";
let _particleData: any = null;
export class Rock {
@ -24,8 +26,8 @@ export class Rock {
constructor(mesh: AbstractMesh) {
this._rockMesh = mesh;
}
public get physicsBody(): PhysicsBody {
return this._rockMesh.physicsBody;
public get physicsBody(): PhysicsBody | null {
return this._rockMesh.physicsBody || null;
}
public get position(): Vector3 {
return this._rockMesh.getAbsolutePosition();
@ -35,6 +37,7 @@ export class Rock {
export class RockFactory {
private static _rockMesh: AbstractMesh;
private static _rockMaterial: PBRMaterial;
private static _originalMaterial: PBRMaterial = null;
private static _explosionPool: ParticleSystemSet[] = [];
private static _poolSize: number = 10;
private static _viewer: PhysicsViewer = null;
@ -64,18 +67,20 @@ export class RockFactory {
//importMesh.meshes[1].dispose();
console.log(importMesh.meshes);
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';
this._rockMaterial.id = 'asteroid-material';
const material = (this._rockMaterial as PBRMaterial)
const noiseTexture = new NoiseProceduralTexture("asteroid-noise", 256, DefaultScene.MainScene);
noiseTexture.brightness = 0.6; // Brighter base color
noiseTexture.octaves = 4; // More detaila
material.albedoTexture = noiseTexture;
material.roughness = 1;
// Create material using GameConfig texture level
const config = GameConfig.getInstance();
this._rockMaterial = MaterialFactory.createAsteroidMaterial(
'asteroid-material',
config.asteroidTextureLevel,
DefaultScene.MainScene,
this._originalMaterial
) as PBRMaterial;
this._rockMesh.material = material;
this._rockMesh.material = this._rockMaterial;
importMesh.meshes[1].dispose(false, true);
importMesh.meshes[0].dispose();
}
@ -104,6 +109,9 @@ export class RockFactory {
rock.metadata = {type: 'asteroid'};
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
// The mesh scaling is already applied, so Babylon will create correctly sized physics shape
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.setLinearVelocity(Vector3.Random(-10, 10));
}
return new Rock(rock);
}
}