@@ -21,6 +21,7 @@ import { QualityMode } from '@/utils/quality';
2121import { DefaultGimmeDistances } from '@/utils/data' ;
2222import { Hole } from './types' ;
2323import { LakeSurface , RiverSurface , VolumetricClouds } from '@/shaders' ;
24+ import { LightmapMaterial , applyLightmapShadow , configureLightmapTexture } from '@/shaders/lightmap' ;
2425import { FuseRenderer } from '@/renderer' ;
2526import { type GolfBall } from '@/objects/golfBall' ;
2627import { PuttingGridMaterial } from '@/shaders/putting' ;
@@ -72,13 +73,14 @@ type MeshLoaderOptions = {
7273}
7374export class MeshLoader extends EventEmitter < CourseLoaderEvents > {
7475 gltfLoader : GLTFLoader ;
75-
76+ ktx2Loader : KTX2Loader ;
77+
7678 constructor ( renderer : FuseRenderer , manager ?: THREE . LoadingManager , options : MeshLoaderOptions = { } ) {
7779 super ( ) ;
7880 const ktx2Path = options . ktx2Path ?? '/ktx2/' ;
79- const ktx2Loader = new KTX2Loader ( ) . setTranscoderPath ( ktx2Path ) . detectSupport ( renderer . renderer ) ;
81+ this . ktx2Loader = new KTX2Loader ( ) . setTranscoderPath ( ktx2Path ) . detectSupport ( renderer . renderer ) ;
8082 this . gltfLoader = new GLTFLoader ( manager ) ;
81- this . gltfLoader . setKTX2Loader ( ktx2Loader ) ;
83+ this . gltfLoader . setKTX2Loader ( this . ktx2Loader ) ;
8284 }
8385
8486 async load ( meshUri : string , firstMeshOnly ?: false ) : Promise < THREE . Group > ;
@@ -207,6 +209,8 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
207209 planter ?: TreePlanter ;
208210 clouds ?: VolumetricClouds ;
209211 light ?: CourseLight ;
212+ lightmaps : Map < string , LightmapMaterial | THREE . Material > ;
213+ #lightmapTexture?: THREE . Texture ;
210214 #renderer: FuseRenderer ;
211215 #camera: ShotPerspectiveCamera ;
212216 #raycaster: THREE . Raycaster ;
@@ -239,7 +243,7 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
239243 this . grasses = new Map ( ) ;
240244 this . greenGrids = new Map ( ) ;
241245 this . #blendMaps = new Map ( ) ;
242-
246+ this . lightmaps = new Map ( ) ;
243247 this . #raycaster = new THREE . Raycaster ( ) ;
244248 this . #origin = new THREE . Vector3 ( ) ;
245249 this . #direction = new THREE . Vector3 ( 0 , - 1 , 0 ) ;
@@ -291,6 +295,7 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
291295 this . _setupCourseSurfaces ( ) ;
292296 this . _parseCourseHoles ( ) ;
293297 this . _addWater ( ) ;
298+ await this . _applyLightmap ( ) ;
294299 await this . _addSkyAndEnvironment ( scene ) ;
295300 await this . _addTrees ( ) ;
296301 await this . _parseMap ( ) ;
@@ -300,6 +305,8 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
300305 // are referenced by the scene, not the parser.)
301306 this . gltf = undefined ;
302307
308+ scene . add ( this . scene ) ;
309+
303310 return this . scene ;
304311 }
305312
@@ -358,9 +365,50 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
358365 }
359366 }
360367
368+ // Baked lightmap — loaded here so grass (built in _setupCourseSurfaces)
369+ // can sample it; surfaces consume it later in _applyLightmap.
370+ const lmImages = parser . json ?. images || [ ] ;
371+ const lmIndex = lmImages . findIndex ( ( img : any ) => img . extras ?. type === 'light_map' ) ;
372+ if ( lmIndex !== - 1 ) {
373+ const lmDef = lmImages [ lmIndex ] as any ;
374+ const lmBuffer = await parser . getDependency ( 'bufferView' , lmDef . bufferView ) ;
375+ let tex : THREE . Texture ;
376+ if ( lmDef . mimeType === 'image/ktx2' ) {
377+ // KTX2-compressed lightmap: decode via the shared KTX2Loader
378+ const blobUrl = URL . createObjectURL ( new Blob ( [ lmBuffer ] ) ) ;
379+ try {
380+ tex = await this . meshLoader . ktx2Loader . loadAsync ( blobUrl ) ;
381+ } finally {
382+ URL . revokeObjectURL ( blobUrl ) ;
383+ }
384+ } else {
385+ // PNG path — courses exported before KTX2 lightmaps
386+ const lmBitmap = await createImageBitmap ( new Blob ( [ lmBuffer ] ) , { premultiplyAlpha : 'none' } ) ;
387+ tex = new THREE . Texture ( lmBitmap ) ;
388+ }
389+ this . #lightmapTexture = configureLightmapTexture ( tex ) ;
390+
391+ }
392+
393+
361394
362395 }
363396
397+ async _applyLightmap ( ) {
398+ if ( ! this . scene ) return ;
399+ const tex = this . #lightmapTexture;
400+ if ( ! tex ) {
401+ console . warn ( 'No shadow map found!' ) ;
402+ return ; // course exported before baking existed — no-op
403+ }
404+ const worldSize = this . courseSize ;
405+
406+ for ( const { mesh } of this . surfaces . values ( ) ) {
407+ const applied = applyLightmapShadow ( mesh , tex , { worldSize } ) ;
408+ if ( applied ) this . lightmaps . set ( mesh . uuid , applied ) ;
409+ }
410+ }
411+
364412 _setupCourseSurfaces ( ) {
365413 if ( ! this . scene ) throw new Error ( 'No scene defined!' ) ;
366414 if ( ! this . gltf ) throw new Error ( 'Course file not loaded' ) ;
@@ -386,6 +434,7 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
386434 if ( ! ( child instanceof THREE . Mesh ) ) { return ; }
387435 if ( ! child . isMesh || ! child . geometry ?. attributes . position ) return ;
388436 child . receiveShadow = true ;
437+ child . castShadow = false ;
389438
390439 // Disable vertex color rendering on all meshes —
391440 // we only use them as data, not visual color
@@ -406,7 +455,7 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
406455
407456 const blendMap = this . #blendMaps. get ( child . userData . id ) ;
408457 if ( blendMap ) {
409-
458+ // TODO: hard-code the neighbor material settings at export time
410459 // const neighborMesh = this.findNeighborMesh(child, allSurfaceMeshes);
411460 // if (neighborMesh && this.grassAssets?.noiseTexture) {
412461 // const sand = new SandMaterial(
@@ -421,19 +470,24 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
421470 // }
422471
423472 } else if ( this . qualityLevel > QualityMode . Medium && surfaceType === 'rough' ) {
473+ let renderDistance = this . qualityLevel === QualityMode . VeryHigh ? 100 : 60 ;
424474 const grassOptions = {
425475 density : 15 ,
426- renderDistance : 50 ,
427- cellSize : 10 ,
476+ clumpSpread : 0.4 ,
477+ lightmap : this . #lightmapTexture,
478+ lightmapWorldSize : this . courseSize ,
479+ renderDistance,
480+ // cellSize: 10,
481+ layer : 2 ,
428482 lean : 0.01 ,
429483 heightVariation : 0.5 ,
430- maxNewCellsPerFrame : 20 ,
484+ // maxNewCellsPerFrame: 20,
431485 scaleXZ : 0.6 ,
432- scaleY : 0.65 ,
433- layer : 2 ,
434- baseColor : new THREE . Color ( '#415722' ) ,
435- tipColor1 : new THREE . Color ( '#5c7c2e' ) ,
436- tipColor2 : new THREE . Color ( '#ffffff' ) ,
486+ scaleY : 0.5 ,
487+ rootDarken : 0.4 ,
488+ // baseColor: new THREE.Color('#415722'),
489+ // tipColor1: new THREE.Color('#5c7c2e'),
490+ // tipColor2: new THREE.Color('#ffffff'),
437491 } ;
438492
439493 // if (this.qualityLevel > QualityMode.Medium) {
@@ -447,16 +501,20 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
447501
448502 } else if ( this . qualityLevel > QualityMode . Medium && [ 'deep_rough' , 'base' ] . includes ( surfaceType ) ) {
449503
504+ let renderDistance = this . qualityLevel === QualityMode . VeryHigh ? 100 : 60 ;
505+
450506 const grass = new GrassShader ( child , this . grassAssets ! , {
451- density : 8 ,
452- renderDistance : 60 ,
453- cellSize : 10 ,
454- lean : 0.03 ,
507+ density : 2 ,
508+ lightmap : this . #lightmapTexture,
509+ lightmapWorldSize : this . courseSize ,
510+ renderDistance,
511+ // cellSize: 10,
455512 layer : 2 ,
456- heightVariation : 0.1 ,
457- maxNewCellsPerFrame : 10 ,
458- scaleXZ : 0.8 ,
459- scaleY : 0.6 ,
513+ lean : 0.03 ,
514+ heightVariation : 0.8 ,
515+ // maxNewCellsPerFrame: 10,
516+ scaleXZ : 0.9 ,
517+ scaleY : 0.7 ,
460518 } ) ;
461519 this . scene . add ( grass . mesh ) ;
462520 this . grasses . set ( child . uuid , grass ) ;
@@ -524,8 +582,6 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
524582 ) ;
525583 const buffer = await parser . getDependency ( 'bufferView' , courseMap . bufferView ) ;
526584 const blob = new Blob ( [ buffer ] , { type : 'image/jpeg' } ) ;
527- const bitmap = await window . createImageBitmap ( blob , { premultiplyAlpha : 'none' } ) ;
528- // this.courseMap = bitmap;
529585 // Probe native dimensions, then release the full-res decode immediately
530586 const probe = await window . createImageBitmap ( blob , { premultiplyAlpha : 'none' } ) ;
531587 const fullW = probe . width ;
@@ -562,8 +618,7 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
562618 qualityLevel : this . qualityLevel ,
563619 // world: this.world,
564620 // rapier: this.rapier,
565- groundMeshes : this . getGroundMeshes ( ) ,
566- refreshShadows : ( ) => this . light ?. refreshShadows ( ) ,
621+ groundMeshes : this . getGroundMeshes ( )
567622 } ) ;
568623
569624 const treeConfigs : Record < string , TreeGroup [ ] > = { } ;
@@ -576,10 +631,10 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
576631 let lodDistances = [ 0 , 40 ] ;
577632 let maxDistance = 500 ;
578633 if ( this . qualityLevel === QualityMode . Medium ) {
579- lodDistances = [ 60 , 100 ] ;
634+ lodDistances = [ 120 , 200 ] ;
580635 maxDistance = 800 ;
581636 } else if ( this . qualityLevel === QualityMode . High ) {
582- lodDistances = [ 200 , 400 ] ;
637+ lodDistances = [ 150 , 200 ] ;
583638 maxDistance = Infinity ;
584639 }
585640 console . log ( `[plant] Planting tree layer... (lods:${ lodDistances . join ( ',' ) } )` , group ) ;
@@ -590,14 +645,14 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
590645 } ,
591646 scaleRange : { min : 1 , max : 1 } ,
592647 density : 1 ,
593- minDistance : 3 ,
648+ minDistance : 8 ,
594649 maxDistance,
595650 lodDistances,
596651 colors : [ ] ,
597652 meshGroup : group ,
598653 ...child . userData
599654 } ;
600-
655+ console . log ( 'config' , config ) ;
601656 if ( ! treeConfigs ?. [ layerId ] ) {
602657 treeConfigs [ layerId ] = [ config ] ;
603658 } else {
@@ -696,7 +751,7 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
696751
697752 let lightOptions = {
698753 qualityLevel : this . qualityLevel ,
699- color : new THREE . Color ( '#fffac0 ' ) ,
754+ color : new THREE . Color ( '#fffcdd ' ) ,
700755 directional : { enabled : true } ,
701756 ambient : { enabled : true }
702757 } ;
@@ -711,12 +766,12 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
711766 cloudColor,
712767 fogColor,
713768 skyColor,
714- position : new THREE . Vector3 ( 0 , - 40 , 0 )
769+ // position: new THREE.Vector3(0, -40, 0)
715770 } ) ;
716771 scene . add ( this . clouds . object ) ;
717772 this . #renderer. generateEnvironment ( scene , this . clouds . object ) ;
718773
719- const fog = new THREE . Fog ( fogColor , 500 , 1200 ) ;
774+ const fog = new THREE . Fog ( fogColor , 500 , 900 ) ;
720775 scene . fog = fog ;
721776 // gameContext.fog = new THREE.Fog(fogColor, 300, 800);
722777 // gameContext.scene.fog = gameContext.fog;
@@ -732,7 +787,7 @@ export class CourseLoader extends EventEmitter<CourseLoaderEvents> {
732787 const buffer : ArrayBuffer = await parser . getDependency ( 'bufferView' , skyboxDef . bufferView ) ;
733788 const box = new SkyBox ( ) ;
734789 box . load ( scene , buffer ) ;
735- scene . environmentIntensity = 0.25 ;
790+ scene . environmentIntensity = 0.15 ;
736791 }
737792 }
738793
0 commit comments