Skip to content

Commit 45997db

Browse files
feat: Add webgpu support and fix bugs (#1)
1 parent d06798d commit 45997db

21 files changed

Lines changed: 406 additions & 230 deletions

File tree

examples/cornhole/cornhole.js

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import '@/css/base.css';
22
import './cornhole.css';
33

4+
const FIXED_DT = 1 / 60;
5+
const MAX_SUBSTEPS = 5; // cap to prevent "spiral of death"
6+
47
import {
58
CourseKeyboardControls,
69
MeshLoader,
@@ -11,7 +14,8 @@ import {
1114
VolumetricClouds,
1215
app,
1316
generateSetupData,
14-
UILoadingScreen
17+
UILoadingScreen,
18+
FuseRenderer
1519
} from '@opengolfsim/fuse';
1620
import { Water } from 'three/examples/jsm/Addons.js';
1721
import groundBeachModel from './models/GroundBeach.glb?url';
@@ -28,7 +32,7 @@ const stopThreshold = 0.05;
2832
const baseBoardDistance = 7;
2933
let boardZOffset = 10;
3034

31-
const fogColor = new THREE.Color('#fffcee');
35+
const fogColor = new THREE.Color('#bddefc');
3236
const skyColor = new THREE.Color('#bddefc');
3337

3438
const textureLoader = new THREE.TextureLoader();
@@ -201,7 +205,7 @@ async function loadGameBoards() {
201205
setupBoard('red', boardMeshOriginal);
202206
}
203207

204-
function setupScene() {
208+
async function setupScene() {
205209
gameContext.scene = new THREE.Scene();
206210
gameContext.scene.background = new THREE.Color(skyColor);
207211
gameContext.scene.fog = new THREE.Fog(fogColor, 10, 140);
@@ -223,8 +227,6 @@ function setupScene() {
223227
gameContext.scene.add(directionalLight);
224228
directionalLight.target.position.set(0, 0, 0);
225229

226-
227-
228230
const waterGroup = new THREE.Group();
229231
const underwaterGeometry = new THREE.PlaneGeometry(300, 100);
230232
const underwaterMaterial = new THREE.MeshBasicMaterial({ color: new THREE.Color('#1a5972') });
@@ -239,8 +241,8 @@ function setupScene() {
239241
const waterGeometry = new THREE.PlaneGeometry(300, 100);
240242

241243
gameContext.water.object = new Water(waterGeometry, {
242-
textureWidth: 512,
243-
textureHeight: 512,
244+
textureWidth: 256,
245+
textureHeight: 256,
244246
waterNormals: textureLoader.load(
245247
waterNormals,
246248
(texture) => {
@@ -278,6 +280,7 @@ function setupScene() {
278280
gameContext.scene.add(waterGroup);
279281

280282

283+
281284
}
282285

283286
function createSky() {
@@ -309,7 +312,7 @@ async function createGround(width = 100, depth = 100) {
309312
tex.wrapT = THREE.RepeatWrapping;
310313
tex.repeat.set(100, 100); // tile 50x across, 100x down the 100x200 plane
311314
tex.colorSpace = THREE.SRGBColorSpace; // correct color rendering
312-
tex.anisotropy = gameContext.renderer.capabilities.getMaxAnisotropy();
315+
tex.anisotropy = gameContext.renderer.getMaxAnisotropy();
313316

314317
const floorMaterial = new THREE.MeshStandardMaterial({
315318
map: tex,
@@ -321,6 +324,7 @@ async function createGround(width = 100, depth = 100) {
321324
// const groundMesh = await loadMesh('models/GroundBeach.glb', true);
322325
const groundMesh = await gameContext.meshLoader?.load(groundBeachModel, true);
323326
console.log('groundMesh', groundMesh.geometry);
327+
// const geo = new THREE.PlaneGeometry(100, 100);
324328

325329
const mesh = new THREE.Mesh(groundMesh.geometry, floorMaterial);
326330
// floor.rotation.x = -Math.PI / 2;
@@ -469,14 +473,22 @@ async function setupGame() {
469473
launchShot({ ballSpeed: 12 + (Math.random() * 2), verticalLaunchAngle: 35, horizontalLaunchAngle: -5 + (Math.random() * 10) });
470474
});
471475

472-
gameContext.renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true });
473-
gameContext.renderer.setSize(window.innerWidth, window.innerHeight);
474-
gameContext.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
475-
gameContext.renderer.shadowMap.enabled = true;
476-
gameContext.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
476+
const canvas = document.getElementById('canvas');
477+
if (!canvas) {
478+
throw new Error('Missing canvas!');
479+
}
480+
gameContext.renderer = new FuseRenderer({
481+
canvas,
482+
antialias: true
483+
// renderMode: 'webgpu'
484+
});
485+
// gameContext.renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true });
486+
// gameContext.renderer.setSize(window.innerWidth, window.innerHeight);
487+
// gameContext.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
488+
// gameContext.renderer.shadowMap.enabled = true;
489+
// gameContext.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
490+
477491

478-
gameContext.meshLoader = new MeshLoader(gameContext.renderer);
479-
480492
// window.addEventListener('resize', () => {
481493
// if (gameContext.camera) {
482494
// gameContext.camera.aspect = window.innerWidth / window.innerHeight;
@@ -486,14 +498,23 @@ async function setupGame() {
486498
// });
487499

488500

489-
setupScene();
490-
await createGround();
491-
gameContext.camera = new ShotPerspectiveCamera(gameContext.renderer, gameContext.ground.mesh, {
501+
await setupScene();
502+
503+
gameContext.camera = new ShotPerspectiveCamera({
504+
canvas,
492505
fov: 30,
493506
cameraOffsetX: 0,
494507
cameraOffsetYZ: [1.5, 1],
495508
});
496509

510+
await gameContext.renderer.init();
511+
512+
gameContext.meshLoader = new MeshLoader(gameContext.renderer.renderer);
513+
514+
await createGround();
515+
516+
gameContext.camera.setScene(gameContext.ground.mesh);
517+
497518
const geo = new THREE.BoxGeometry(0.06, 2, 0.06);
498519
const mat = new THREE.MeshBasicMaterial({ color: 'red', transparent: true, opacity: 0.8 });
499520

@@ -504,7 +525,7 @@ async function setupGame() {
504525
gameContext.scene.add(gameContext.aimMesh);
505526

506527
gameContext.camera?.setPositions(gameContext.startPoint, gameContext.aimPoint);
507-
createSky();
528+
// createSky();
508529

509530

510531
await loadGameBoards();
@@ -539,12 +560,13 @@ function loadGame() {
539560
gameContext.timer.connect(document);
540561

541562
gameContext.loadingScreen = new UILoadingScreen(document.body, { loadingPrefix: 'Filling the bags' });
542-
gameContext.loadingScreen.on('load', () => {
563+
gameContext.loadingScreen.on('load', async () => {
543564
gameContext.clock.start();
544565
requestAnimationFrame(animate);
545566
});
546567
gameContext.loadingScreen.load(setupGame);
547568

569+
document.body.style.opacity = '1';
548570

549571
// requestAnimationFrame(animate);
550572
}
@@ -767,25 +789,36 @@ function animate(animDelta) {
767789
const delta = gameContext.timer.getDelta();
768790
gameContext.timer.update(animDelta);
769791

770-
app.world.step(gameContext.eventQueue);
771-
gameContext.clouds.update();
772-
773-
gameContext.eventQueue.drainCollisionEvents((h1, h2, started) => {
774-
if (!started) return; // only care about entering, not leaving
775-
const blueHole = gameContext.boards.blue.colliderHole.handle;
776-
const redHole = gameContext.boards.red.colliderHole.handle;
792+
// fixed-rate physics
793+
gameContext.accumulator += delta;
777794

778-
let holeTeam = null;
779-
if (h1 === blueHole || h2 === blueHole) holeTeam = 'blue';
780-
else if (h1 === redHole || h2 === redHole) holeTeam = 'red';
781-
if (!holeTeam) return;
795+
// Clamp so a long hitch doesn't queue dozens of steps
796+
if (gameContext.accumulator > FIXED_DT * MAX_SUBSTEPS) {
797+
gameContext.accumulator = FIXED_DT * MAX_SUBSTEPS;
798+
}
782799

783-
const bagHandle = (h1 === blueHole || h1 === redHole) ? h2 : h1;
784-
const bag = gameContext.bags.find(b => b.collider.handle === bagHandle);
785-
if (bag) {
786-
bag.inHole = holeTeam;
787-
}
788-
});
800+
while (gameContext.accumulator >= FIXED_DT) {
801+
app.world.timestep = FIXED_DT;
802+
app.world.step(gameContext.eventQueue);
803+
gameContext.accumulator -= FIXED_DT;
804+
805+
// Drain collision events *inside* the loop so you don't
806+
// miss events from intermediate substeps
807+
gameContext.eventQueue.drainCollisionEvents((h1, h2, started) => {
808+
if (!started) return;
809+
const blueHole = gameContext.boards.blue.colliderHole.handle;
810+
const redHole = gameContext.boards.red.colliderHole.handle;
811+
812+
let holeTeam = null;
813+
if (h1 === blueHole || h2 === blueHole) holeTeam = 'blue';
814+
else if (h1 === redHole || h2 === redHole) holeTeam = 'red';
815+
if (!holeTeam) return;
816+
817+
const bagHandle = (h1 === blueHole || h1 === redHole) ? h2 : h1;
818+
const bag = gameContext.bags.find(b => b.collider.handle === bagHandle);
819+
if (bag) bag.inHole = holeTeam;
820+
});
821+
}
789822

790823
// Sync meshes to physics
791824
for (const bag of gameContext.bags) {
@@ -835,13 +868,11 @@ function animate(animDelta) {
835868
gameContext.startPoint,
836869
gameContext.aimPoint
837870
);
838-
if (aimChanged) {
839-
// gameContext.aimPoint.copy();
840-
}
841871

842872
if (gameContext.aimMesh) gameContext.aimMesh.position.copy(gameContext.aimPoint);
843873

844-
gameContext.camera?.render(gameContext.scene, gameContext.fog);
874+
// gameContext.camera?.render(gameContext.scene, gameContext.fog);
875+
gameContext.renderer?.render(gameContext.scene, gameContext.camera, gameContext.fog);
845876

846877
gameContext.stats?.end();
847878
}

examples/cornhole/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
77
<script type="module" src="./cornhole.js"></script>
88
</head>
9-
<body>
9+
<body style="opacity: 0">
1010

1111
<canvas id="canvas"></canvas>
1212

examples/courses/courses.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
VolumetricClouds,
2020
generateSetupData,
2121
UIMainMenu,
22+
FuseRenderer,
2223
} from '@opengolfsim/fuse';
2324

2425

@@ -31,7 +32,7 @@ const gameContext: {
3132
timer: THREE.Timer,
3233
world?: World;
3334
scene?: THREE.Scene;
34-
renderer?: THREE.WebGLRenderer,
35+
renderer?: FuseRenderer,
3536
golfBall?: GolfBall,
3637
lightGroup?: CourseLight,
3738
fog?: THREE.Fog,
@@ -112,18 +113,19 @@ function setupNextShot() {
112113
}
113114

114115
function setupRenderer() {
115-
const canvas = document.getElementById('canvas');
116-
if (!canvas) throw new Error('Unable to find canvas in HTML. Make sure you create a root canvas element (e.g. <canvas id="canvas"></canvas>)');
117116

118117
THREE.ColorManagement.enabled = true;
119-
118+
120119
app.sendMessage({ type: 'log', message: `qualityLevel: ${gameContext.qualityLevel}` });
121-
122-
gameContext.renderer = new THREE.WebGLRenderer({
120+
121+
const canvas = document.getElementById('canvas');
122+
if (!canvas || !(canvas instanceof HTMLCanvasElement)) throw new Error('Unable to find canvas in HTML. Make sure you create a root canvas element (e.g. <canvas id="canvas"></canvas>)');
123+
gameContext.renderer = new FuseRenderer({
123124
canvas,
125+
qualityLevel: gameContext.qualityLevel,
124126
antialias: true // gameContext.qualityLevel >= QualityMode.Medium
125127
});
126-
gameContext.renderer.setSize(window.innerWidth, window.innerHeight);
128+
// gameContext.renderer.setSize(window.innerWidth, window.innerHeight);
127129

128130
let maxPixelRatio = Math.min(window.devicePixelRatio, 1);
129131
if (gameContext.qualityLevel >= QualityMode.High) {
@@ -132,14 +134,10 @@ function setupRenderer() {
132134

133135
app.sendMessage({ type: 'log', message: `maxPixelRatio: ${maxPixelRatio}` });
134136

135-
gameContext.renderer.setPixelRatio(maxPixelRatio);
136-
gameContext.renderer.shadowMap.enabled = gameContext.qualityLevel >= QualityMode.Medium;
137-
gameContext.renderer.shadowMap.type = THREE.PCFShadowMap;
137+
// gameContext.renderer.setPixelRatio(maxPixelRatio);
138+
// gameContext.renderer.shadowMap.enabled = gameContext.qualityLevel >= QualityMode.Medium;
139+
// gameContext.renderer.shadowMap.type = THREE.PCFShadowMap;
138140

139-
if (gameContext.qualityLevel >= QualityMode.Medium) {
140-
gameContext.renderer.toneMapping = THREE.ACESFilmicToneMapping; // or whatever you pick
141-
gameContext.renderer.toneMappingExposure = 1.0;
142-
}
143141

144142
}
145143

@@ -169,10 +167,11 @@ async function setupScene() {
169167
if (!gameContext.course) {
170168
throw new Error('Course object does not exist!');
171169
}
170+
const ground = gameContext.course.getGroundMeshes();
171+
console.log('ground', ground);
172172
gameContext.camera = new ShotPerspectiveCamera(
173-
gameContext.renderer,
174-
gameContext.course.getGroundMeshes(),
175173
{
174+
scene: ground,
176175
cameraOffsetX: (gameContext.setupData?.cameraOffset ? -(gameContext.setupData.cameraOffset / 100) : 0),
177176
}
178177
);
@@ -218,6 +217,8 @@ async function setupScene() {
218217
});
219218
gameContext.scene.add(gameContext.clouds.object);
220219

220+
221+
221222
}
222223

223224
/**
@@ -294,9 +295,10 @@ async function setupCourse() {
294295
gameContext.course = new CourseLoader(
295296
app.world,
296297
app.rapier,
297-
gameContext.renderer,
298+
gameContext.renderer.renderer,
298299
{
299300
setupData: gameContext.setupData,
301+
qualityLevel: gameContext.qualityLevel,
300302
manager: gameContext.loadingScreen?.manager,
301303
meshLoaderOptions: { ktx2Path: '../ktx2/' }
302304
}
@@ -352,6 +354,7 @@ async function setupCourse() {
352354
});
353355

354356

357+
// gameContext.camera?.setScene(gameContext.course.getGroundMeshes());
355358
setupNextShot();
356359

357360
gameContext.courseMap?.on('updateAim', adjustAimPoint);
@@ -375,7 +378,7 @@ function preLoad() {
375378
console.log('[debug] Setup Data', gameContext.setupData);
376379
gameContext.loadingScreen = new UILoadingScreen(document.body, { loadingPrefix: 'Loading Course' });
377380
gameContext.loadingScreen.on('load', (error) => {
378-
gameContext.stats = new UIStats('#render-stats', { hidden: false, renderer: gameContext.renderer }); // start hidden (press S to toggle)
381+
gameContext.stats = new UIStats('#render-stats', { hidden: false, renderer: gameContext.renderer?.renderer }); // start hidden (press S to toggle)
379382
if (!error) {
380383
requestAnimationFrame(animate);
381384
gameContext.isReady = true;
@@ -432,7 +435,10 @@ function animate(animDelta: number) {
432435
aimPointUpdated();
433436
}
434437
}
435-
gameContext.camera?.render(gameContext.scene, gameContext.fog);
438+
// gameContext.camera?.render(gameContext.scene, gameContext.fog);
439+
if (gameContext.camera) {
440+
gameContext.renderer?.render(gameContext.scene, gameContext.camera, gameContext.fog);
441+
}
436442
}
437443
}
438444

examples/range/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<!DOCTYPE html>
22
<html>
33
<head>
4-
<link rel="stylesheet" type="text/css" href="../../src/css/base.css" />
54
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
65
<title>Fuse Range</title>
76
</head>

0 commit comments

Comments
 (0)