Add procedural lightmap system for planets and asteroids
Some checks failed
Build / build (push) Failing after 19s
Some checks failed
Build / build (push) Failing after 19s
- Create sphereLightmap.ts for procedural lighting generation - Update planets to use lightmaps oriented toward sun - Switch asteroids to PBR material with noise texture - Use sphere physics shape for asteroids 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
942c0a1af0
commit
a9054c2389
BIN
public/ship1.blend
Normal file
BIN
public/ship1.blend
Normal file
Binary file not shown.
BIN
public/ship1.glb
Normal file
BIN
public/ship1.glb
Normal file
Binary file not shown.
@ -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<ScoreEvent>
|
||||
): Promise<AbstractMesh[]> {
|
||||
const asteroids: AbstractMesh[] = [];
|
||||
|
||||
@ -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;
|
||||
|
||||
30
src/ship.ts
30
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
148
src/sphereLightmap.ts
Normal file
148
src/sphereLightmap.ts
Normal file
@ -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;
|
||||
}
|
||||
@ -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,6 +51,10 @@ export class RockFactory {
|
||||
console.log(`Created ${this._poolSize} explosion particle systems in pool`);
|
||||
|
||||
if (!this._rockMesh) {
|
||||
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);
|
||||
@ -60,25 +65,21 @@ export class RockFactory {
|
||||
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;
|
||||
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;
|
||||
|
||||
|
||||
|
||||
//material.albedoColor = new Color3(1, 1, 1);
|
||||
//material.emissiveColor = new Color3(1, 1, 1);
|
||||
this._rockMesh.material = this._rockMaterial;
|
||||
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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user