diff --git a/public/ship1.blend b/public/ship1.blend new file mode 100644 index 0000000..6e69725 Binary files /dev/null and b/public/ship1.blend differ diff --git a/public/ship1.glb b/public/ship1.glb new file mode 100644 index 0000000..ab61fba Binary files /dev/null and b/public/ship1.glb differ diff --git a/src/levelDeserializer.ts b/src/levelDeserializer.ts index c0d7b86..6d9e4ea 100644 --- a/src/levelDeserializer.ts +++ b/src/levelDeserializer.ts @@ -26,6 +26,7 @@ import { validateLevelConfig } from "./levelConfig"; import { FireProceduralTexture } from "@babylonjs/procedural-textures"; +import {createSphereLightmap} from "./sphereLightmap"; /** * Deserializes a LevelConfig JSON object and creates all entities in the scene @@ -59,7 +60,7 @@ export class LevelDeserializer { const startBase = this.createStartBase(); const sun = this.createSun(); const planets = this.createPlanets(); - const asteroids = await this.createAsteroids(startBase, scoreObservable); + const asteroids = await this.createAsteroids(scoreObservable); return { startBase, @@ -104,8 +105,8 @@ export class LevelDeserializer { const config = this.config.sun; // Create point light - const light = new PointLight("light", this.arrayToVector3(config.position), this.scene); - light.intensity = config.intensity || 1000000; + //const light = new PointLight("light", this.arrayToVector3(config.position), this.scene); + //light.intensity = config.intensity || 1000000; // Create sun sphere const sun = MeshBuilder.CreateSphere("sun", { @@ -123,8 +124,8 @@ export class LevelDeserializer { sun.material = material; // Create glow layer - const gl = new GlowLayer("glow", this.scene); - gl.intensity = 1; + //const gl = new GlowLayer("glow", this.scene); + //gl.intensity = 1; return sun; } @@ -134,6 +135,7 @@ export class LevelDeserializer { */ private createPlanets(): AbstractMesh[] { const planets: AbstractMesh[] = []; + const sunPosition = this.arrayToVector3(this.config.sun.position); for (const planetConfig of this.config.planets) { const planet = MeshBuilder.CreateSphere(planetConfig.name, { @@ -141,17 +143,37 @@ export class LevelDeserializer { segments: 32 }, this.scene); - planet.position = this.arrayToVector3(planetConfig.position); + const planetPosition = this.arrayToVector3(planetConfig.position); + planet.position = planetPosition; - if (planetConfig.rotation) { - planet.rotation = this.arrayToVector3(planetConfig.rotation); - } + // 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); - material.diffuseTexture = texture; - material.ambientTexture = texture; + + // Create lightmap with bright light pointing toward sun + const lightmap = createSphereLightmap( + planetConfig.name + "-lightmap", + 512, // 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 + ); + + // 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; @@ -167,7 +189,6 @@ export class LevelDeserializer { * Create asteroids from config */ private async createAsteroids( - startBase: AbstractMesh, scoreObservable: Observable ): Promise { const asteroids: AbstractMesh[] = []; diff --git a/src/main.ts b/src/main.ts index 640aa4e..d6c9c2c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import type {AudioEngineV2} from "@babylonjs/core"; +import {AudioEngineV2, DirectionalLight} from "@babylonjs/core"; import { Color3, CreateAudioEngineAsync, @@ -170,9 +170,10 @@ export class Main { private async setupPhysics() { const havok = await HavokPhysics(); const havokPlugin = new HavokPlugin(true, havok); - + 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/45); + DefaultScene.MainScene.getPhysicsEngine().setTimeStep(1/30); DefaultScene.MainScene.getPhysicsEngine().setSubTimeStep(5); DefaultScene.MainScene.collisionsEnabled = true; diff --git a/src/ship.ts b/src/ship.ts index 85eb1a3..0bff303 100644 --- a/src/ship.ts +++ b/src/ship.ts @@ -1,5 +1,5 @@ import { - AbstractMesh, + AbstractMesh, Angle, Color3, DirectionalLight, FreeCamera, @@ -103,7 +103,7 @@ export class Ship { ammo.position.y = 2; ammo.rotation.x = Math.PI / 2; ammo.setParent(null); - const ammoAggregate = new PhysicsAggregate(ammo, PhysicsShapeType.CONVEX_HULL, { + const ammoAggregate = new PhysicsAggregate(ammo, PhysicsShapeType.SPHERE, { mass: 1000, restitution: 0 }, DefaultScene.MainScene); @@ -118,7 +118,7 @@ export class Ship { window.setTimeout(() => { ammoAggregate.dispose(); ammo.dispose() - }, 1500); + }, 2000); } public set position(newPosition: Vector3) { @@ -141,7 +141,7 @@ export class Ship { } this._ammoMaterial = new StandardMaterial("ammoMaterial", DefaultScene.MainScene); this._ammoMaterial.emissiveColor = new Color3(1, 1, 0); - this._ammoBaseMesh = MeshBuilder.CreateCapsule("bullet", {radius: .1, height: 2.5}, DefaultScene.MainScene); + this._ammoBaseMesh = MeshBuilder.CreateSphere("bullet", {diameter: .2}, DefaultScene.MainScene); this._ammoBaseMesh.material = this._ammoMaterial; this._ammoBaseMesh.setEnabled(false); @@ -176,33 +176,37 @@ export class Ship { DefaultScene.MainScene.setActiveCameraByName("Flat Camera"); //const sightPos = this._forwardNode.position.scale(30); - const sight = MeshBuilder.CreateSphere("sight", {diameter: 1}, DefaultScene.MainScene); + const sight = MeshBuilder.CreateDisc("sight", {radius: 2 }, DefaultScene.MainScene); + sight.parent = this._ship + //sight.rotation.x = -Math.PI / 2; const signtMaterial = new StandardMaterial("sightMaterial", DefaultScene.MainScene); signtMaterial.emissiveColor = Color3.Yellow(); signtMaterial.ambientColor = Color3.Yellow(); sight.material = signtMaterial; sight.position = new Vector3(0, 2, 125); - let i = Date.now(); + sight.renderingGroupId = 3; + let i = 0; DefaultScene.MainScene.onBeforeRenderObservable.add(() => { - if (Date.now() - i > 50 && this._active == true) { + if (i++ % 10 == 0) { this.applyForce(); - i = Date.now(); } }); + this._active = true; } private async initialize() { - const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "cockpit2.glb", DefaultScene.MainScene); + const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "ship1.glb", DefaultScene.MainScene); const shipMesh = importMesh.meshes[0]; shipMesh.id = "shipMesh"; shipMesh.name = "shipMesh"; shipMesh.parent = this._ship; - shipMesh.rotation.y = Math.PI; - shipMesh.position.y = 1; + //shipMesh.rotation.y = Angle.FromDegrees(90).radians(); + //shipMesh.rotation.y = Math.PI; + //shipMesh.position.y = 1; shipMesh.position.z = -1; shipMesh.renderingGroupId = 3; - const light = new PointLight("ship.light", new Vector3(0, 1, .9), DefaultScene.MainScene); + const light = new PointLight("ship.light", new Vector3(0, .5, .1), DefaultScene.MainScene); light.intensity = 4; light.includedOnlyMeshes = [shipMesh]; for (const mesh of shipMesh.getChildMeshes()) { @@ -212,7 +216,7 @@ export class Ship { } } light.parent = this._ship; - DefaultScene.MainScene.getMaterialById('glass_mat.002').alpha = .4; + //DefaultScene.MainScene.getMaterialById('glass_mat.002').alpha = .4; } diff --git a/src/sphereLightmap.ts b/src/sphereLightmap.ts new file mode 100644 index 0000000..7214cd9 --- /dev/null +++ b/src/sphereLightmap.ts @@ -0,0 +1,148 @@ +import { DynamicTexture, Scene, Vector3 } from "@babylonjs/core"; + +/** + * Generate a lightmap texture for a sphere with two directional lights + * @param name - Texture name + * @param size - Texture resolution (e.g., 512, 1024) + * @param scene - Babylon scene + * @param brightLightDir - Direction of bright light (will be normalized) + * @param brightIntensity - Intensity of bright light (0-1) + * @param dimLightDir - Direction of dim light (will be normalized) + * @param dimIntensity - Intensity of dim light (0-1) + * @param ambientIntensity - Base ambient light (0-1) + * @returns DynamicTexture with baked lighting + */ +export function createSphereLightmap( + name: string, + size: number, + scene: Scene, + brightLightDir: Vector3 = new Vector3(1, 0, 0), + brightIntensity: number = 1.0, + dimLightDir: Vector3 = new Vector3(-1, 0, 0), + dimIntensity: number = 0.2, + ambientIntensity: number = 0.1 +): DynamicTexture { + const texture = new DynamicTexture(name, { width: size, height: size }, scene, false); + const context = texture.getContext(); + const imageData = context.createImageData(size, size); + + // Normalize light directions + const brightDir = brightLightDir.normalize(); + const dimDir = dimLightDir.normalize(); + + // Generate lightmap + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + // Convert pixel coordinates to UV (0-1) + const u = x / (size - 1); + const v = y / (size - 1); + + // Convert UV to 3D position on unit sphere + // Using spherical coordinates: theta (longitude), phi (latitude) + const theta = u * Math.PI * 2; // 0 to 2π + const phi = v * Math.PI; // 0 to π + + // Convert spherical to Cartesian (unit sphere) + const normal = new Vector3( + Math.sin(phi) * Math.cos(theta), + Math.cos(phi), + Math.sin(phi) * Math.sin(theta) + ); + + // Calculate lighting from bright light + // Lambertian diffuse: max(0, dot(normal, lightDir)) + const brightDot = Vector3.Dot(normal, brightDir); + const brightLight = Math.max(0, brightDot) * brightIntensity; + + // Calculate lighting from dim light + const dimDot = Vector3.Dot(normal, dimDir); + const dimLight = Math.max(0, dimDot) * dimIntensity; + + // Combine all lighting + const totalLight = ambientIntensity + brightLight + dimLight; + + // Clamp to 0-1 range + const intensity = Math.min(1, Math.max(0, totalLight)); + + // Convert to 0-255 grayscale + const brightness = Math.floor(intensity * 255); + + // Set pixel (RGBA) + const index = (y * size + x) * 4; + imageData.data[index + 0] = brightness; // R + imageData.data[index + 1] = brightness; // G + imageData.data[index + 2] = brightness; // B + imageData.data[index + 3] = 255; // A (fully opaque) + } + } + + // Write image data to texture + context.putImageData(imageData, 0, 0); + texture.update(); + + return texture; +} + +/** + * Create a colored lightmap with tinted lights + */ +export function createColoredSphereLightmap( + name: string, + size: number, + scene: Scene, + brightLightDir: Vector3 = new Vector3(1, 0, 0), + brightColor: { r: number; g: number; b: number } = { r: 1, g: 1, b: 0.8 }, + brightIntensity: number = 1.0, + dimLightDir: Vector3 = new Vector3(-1, 0, 0), + dimColor: { r: number; g: number; b: number } = { r: 0.3, g: 0.3, b: 0.5 }, + dimIntensity: number = 0.2, + ambientColor: { r: number; g: number; b: number } = { r: 0.1, g: 0.1, b: 0.1 } +): DynamicTexture { + const texture = new DynamicTexture(name, { width: size, height: size }, scene, false); + const context = texture.getContext(); + const imageData = context.createImageData(size, size); + + const brightDir = brightLightDir.normalize(); + const dimDir = dimLightDir.normalize(); + + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const u = x / (size - 1); + const v = y / (size - 1); + + const theta = u * Math.PI * 2; + const phi = v * Math.PI; + + const normal = new Vector3( + Math.sin(phi) * Math.cos(theta), + Math.cos(phi), + Math.sin(phi) * Math.sin(theta) + ); + + // Calculate lighting from each source + const brightDot = Math.max(0, Vector3.Dot(normal, brightDir)) * brightIntensity; + const dimDot = Math.max(0, Vector3.Dot(normal, dimDir)) * dimIntensity; + + // Combine colored lights + const r = ambientColor.r + (brightColor.r * brightDot) + (dimColor.r * dimDot); + const g = ambientColor.g + (brightColor.g * brightDot) + (dimColor.g * dimDot); + const b = ambientColor.b + (brightColor.b * brightDot) + (dimColor.b * dimDot); + + // Clamp and convert to 0-255 + const red = Math.floor(Math.min(1, Math.max(0, r)) * 255); + const green = Math.floor(Math.min(1, Math.max(0, g)) * 255); + const blue = Math.floor(Math.min(1, Math.max(0, b)) * 255); + + const index = (y * size + x) * 4; + imageData.data[index + 0] = red; + imageData.data[index + 1] = green; + imageData.data[index + 2] = blue; + imageData.data[index + 3] = 255; + } + } + + context.putImageData(imageData, 0, 0); + texture.update(); + + return texture; +} diff --git a/src/starfield.ts b/src/starfield.ts index 6c48d19..55301da 100644 --- a/src/starfield.ts +++ b/src/starfield.ts @@ -2,7 +2,7 @@ import { AbstractMesh, Color3, InstancedMesh, Mesh, - MeshBuilder, Observable, + MeshBuilder, NoiseProceduralTexture, Observable, ParticleHelper, ParticleSystem, ParticleSystemSet, @@ -10,12 +10,13 @@ import { PhysicsAggregate, PhysicsBody, PhysicsMotionType, PhysicsShapeType, PhysicsViewer, - SceneLoader, + SceneLoader, StandardMaterial, Vector3 } from "@babylonjs/core"; import {DefaultScene} from "./defaultScene"; import {ScoreEvent} from "./scoreboard"; import {Debug} from "@babylonjs/core/Legacy/legacy"; +import {createSphereLightmap} from "./sphereLightmap"; let _particleData: any = null; export class Rock { @@ -50,35 +51,35 @@ export class RockFactory { console.log(`Created ${this._poolSize} explosion particle systems in pool`); if (!this._rockMesh) { - console.log('loading mesh'); - const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "asteroid2.glb", DefaultScene.MainScene); - this._rockMesh = importMesh.meshes[1].clone("asteroid", null, false); - this._rockMesh.setParent(null); - this._rockMesh.setEnabled(false); - - //importMesh.meshes[1].dispose(); - console.log(importMesh.meshes); - if (!this._rockMaterial) { - this._rockMaterial = this._rockMesh.material.clone("asteroid") as PBRMaterial; - this._rockMaterial.name = 'asteroid-material'; - this._rockMaterial.id = 'asteroid-material'; - const material = (this._rockMaterial as PBRMaterial) - //material.albedoTexture = null; - //material.ambientColor = new Color3(.4, .4 ,.4); - material.albedoColor = new Color3(.4, .4 ,.4); - //material.ambientTexture = material.albedoTexture; - - - - //material.albedoColor = new Color3(1, 1, 1); - //material.emissiveColor = new Color3(1, 1, 1); - this._rockMesh.material = this._rockMaterial; - importMesh.meshes[1].dispose(false, true); - importMesh.meshes[0].dispose(); - } + await this.loadMesh(); } } + private static async loadMesh() { + console.log('loading mesh'); + const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "asteroid2.glb", DefaultScene.MainScene); + this._rockMesh = importMesh.meshes[1].clone("asteroid", null, false); + this._rockMesh.setParent(null); + this._rockMesh.setEnabled(false); + //importMesh.meshes[1].dispose(); + console.log(importMesh.meshes); + if (!this._rockMaterial) { + this._rockMaterial = this._rockMesh.material.clone("asteroid") as PBRMaterial; + + 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; + + this._rockMesh.material = material; + importMesh.meshes[1].dispose(false, true); + importMesh.meshes[0].dispose(); + } + } private static getExplosionFromPool(): ParticleSystemSet | null { return this._explosionPool.pop() || null; } @@ -102,19 +103,22 @@ export class RockFactory { rock.id = "asteroid-" + i; rock.metadata = {type: 'asteroid'}; rock.setEnabled(true); - console.log(rock.getBoundingInfo()); - const agg = new PhysicsAggregate(rock, PhysicsShapeType.CONVEX_HULL, { + + // 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, { mass: 10000, - restitution: .5, + restitution: .5 + // Don't pass radius - let Babylon compute from scaled mesh bounds }, DefaultScene.MainScene); - const body =agg.body; + const body = agg.body; if (!this._viewer) { - this._viewer = new PhysicsViewer(DefaultScene.MainScene); + // this._viewer = new PhysicsViewer(DefaultScene.MainScene); } - //this._viewer.showBody(body); - body.setLinearDamping(0); + // this._viewer.showBody(body); + body.setLinearDamping(0) body.setMotionType(PhysicsMotionType.DYNAMIC); body.setCollisionCallbackEnabled(true); let scaling = Vector3.One();