@@ -54,6 +54,18 @@ const create3DEnvironment = () => {
5454 rimLight . penumbra = 0.5 ;
5555 scene . add ( rimLight ) ;
5656
57+ // Extra key light from right-above to brighten top surfaces in showroom mode
58+ const topRightLight = new THREE . SpotLight ( 0xffffff , 1.1 ) ;
59+ topRightLight . position . set ( 2.8 , 10 , 0.8 ) ;
60+ topRightLight . angle = Math . PI / 4 ;
61+ topRightLight . penumbra = 0.35 ;
62+ topRightLight . decay = 2 ;
63+ topRightLight . distance = 35 ;
64+ topRightLight . castShadow = true ;
65+ topRightLight . shadow . mapSize . width = 1024 ;
66+ topRightLight . shadow . mapSize . height = 1024 ;
67+ scene . add ( topRightLight ) ;
68+
5769 // Add a lighter ground plane (showroom floor)
5870 const groundGeometry = new THREE . PlaneGeometry ( 50 , 50 ) ;
5971 const groundMaterial = new THREE . MeshStandardMaterial ( {
@@ -129,8 +141,14 @@ const create3DEnvironment = () => {
129141 let roadSpeed = 0 ; // Actual animation speed (calculated from currentSpeed)
130142 let isDayMode = true ; // Day/night mode toggle
131143
144+ // Resolve controls constructor across different global script builds.
145+ const OrbitControlsCtor = THREE . OrbitControls || window . OrbitControls ;
146+ if ( ! OrbitControlsCtor ) {
147+ throw new Error ( 'OrbitControls failed to load. Check index.html script URLs.' ) ;
148+ }
149+
132150 // Add OrbitControls for mouse interaction
133- const controls = new THREE . OrbitControls ( camera , renderer . domElement ) ;
151+ const controls = new OrbitControlsCtor ( camera , renderer . domElement ) ;
134152 controls . enableDamping = true ;
135153 controls . dampingFactor = 0.05 ;
136154 controls . minDistance = 2 ;
@@ -447,6 +465,7 @@ const create3DEnvironment = () => {
447465 spotLight1 . visible = true ;
448466 spotLight2 . visible = true ;
449467 rimLight . visible = true ;
468+ topRightLight . visible = true ;
450469 ambientLight . intensity = 0.2 ;
451470
452471 // Dark showroom background
@@ -469,6 +488,7 @@ const create3DEnvironment = () => {
469488 spotLight1 . visible = false ;
470489 spotLight2 . visible = false ;
471490 rimLight . visible = false ;
491+ topRightLight . visible = false ;
472492
473493 // Apply day/night lighting
474494 if ( isDayMode ) {
@@ -745,7 +765,11 @@ const create3DEnvironment = () => {
745765
746766 // Load a car model from the internet
747767 // Using a free GLTF model from Sketchfab or similar sources
748- const loader = new THREE . GLTFLoader ( ) ;
768+ const GLTFLoaderCtor = THREE . GLTFLoader || window . GLTFLoader ;
769+ if ( ! GLTFLoaderCtor ) {
770+ throw new Error ( 'GLTFLoader failed to load. Check index.html script URLs.' ) ;
771+ }
772+ const loader = new GLTFLoaderCtor ( ) ;
749773
750774 // For demonstration, let's create a simple car with rotating wheels
751775 // You can replace this with a real GLTF model URL
@@ -797,9 +821,10 @@ const create3DEnvironment = () => {
797821 scene . add ( car ) ;
798822 }
799823
800- // Load the local Ferrari F40 GLTF model
824+ // Load the local Sunburst body GLB model
825+ const modelUrl = `sunburst-body/sunburst-body.glb?v=${ Date . now ( ) } ` ;
801826 loader . load (
802- 'ferrari_f40/scene.gltf' , // Local model path
827+ modelUrl , // Local model path (cache-busted)
803828 ( gltf ) => {
804829 console . log ( '✅ Model loaded successfully!' , gltf ) ;
805830
@@ -810,13 +835,131 @@ const create3DEnvironment = () => {
810835 }
811836
812837 car = gltf . scene ;
813- car . scale . set ( 1 , 1 , 1 ) ; // Adjust scale as needed
814- car . position . y = - 0.5 ; // Position on the ground
838+
839+ // Normalize model size/position so different assets still appear in-frame.
840+ const bbox = new THREE . Box3 ( ) . setFromObject ( car ) ;
841+ const size = bbox . getSize ( new THREE . Vector3 ( ) ) ;
842+ const center = bbox . getCenter ( new THREE . Vector3 ( ) ) ;
843+ const maxDim = Math . max ( size . x , size . y , size . z ) ;
844+ const targetSize = 6.5 ;
845+ const scale = maxDim > 0 ? targetSize / maxDim : 1 ;
846+ car . scale . setScalar ( scale ) ;
847+
848+ // Recompute bounds after scaling, then center on X/Z and sit on ground.
849+ const scaledBox = new THREE . Box3 ( ) . setFromObject ( car ) ;
850+ const scaledCenter = scaledBox . getCenter ( new THREE . Vector3 ( ) ) ;
851+ const groundY = - 0.5 ;
852+ car . position . set ( - scaledCenter . x , groundY - scaledBox . min . y , - scaledCenter . z ) ;
853+ car . rotation . y = Math . PI ;
815854
816855 car . traverse ( ( node ) => {
817856 if ( node . isMesh ) {
857+ const nodeName = ( node . name || '' ) . toLowerCase ( ) ;
858+
859+ // Some exports include helper/grid meshes that overlap the body and cause moire artifacts.
860+ if ( nodeName . includes ( 'grid' ) ) {
861+ node . visible = false ;
862+ console . log ( 'Hid helper mesh:' , node . name || '(unnamed mesh)' ) ;
863+ return ;
864+ }
865+
818866 node . castShadow = true ;
819- node . receiveShadow = true ;
867+ // Avoid heavy self-shadow acne/striping on curved body panels.
868+ node . receiveShadow = false ;
869+
870+ const sourceMaterials = Array . isArray ( node . material ) ? node . material : [ node . material ] ;
871+ const adjustedMaterials = sourceMaterials . map ( ( srcMat ) => {
872+ if ( ! srcMat ) return srcMat ;
873+ const mat = srcMat . clone ( ) ;
874+ const matName = ( mat . name || '' ) . toLowerCase ( ) ;
875+
876+ // Make glass meshes clearly visible with alpha blending.
877+ const isGlass = nodeName . includes ( 'glass' ) || nodeName . includes ( 'windshield' ) || matName . includes ( 'glass' ) || matName . includes ( 'windshield' ) ;
878+ if ( isGlass ) {
879+ mat . transparent = true ;
880+ mat . opacity = 0.55 ;
881+ mat . alphaTest = 0.0 ;
882+ mat . depthWrite = false ;
883+ mat . side = THREE . DoubleSide ;
884+ if ( mat . color && mat . color . setRGB ) {
885+ mat . color . setRGB ( 0.75 , 0.88 , 1.0 ) ;
886+ }
887+ if ( typeof mat . transmission === 'number' ) {
888+ mat . transmission = 0.7 ;
889+ }
890+ if ( typeof mat . roughness === 'number' ) {
891+ mat . roughness = 0.1 ;
892+ }
893+ if ( typeof mat . metalness === 'number' ) {
894+ mat . metalness = 0.0 ;
895+ }
896+
897+ // Glass should not cast hard shadows.
898+ node . castShadow = false ;
899+ console . log ( 'Adjusted glass material:' , node . name || '(unnamed mesh)' ) ;
900+ }
901+
902+ // Improve solar panel tile readability (reduce z-fighting + texture blur).
903+ const isSolar = nodeName . includes ( 'solar' ) || matName . includes ( 'solar' ) || matName . includes ( 'cell' ) ;
904+ if ( isSolar ) {
905+ // Keep solar surfaces stable and readable.
906+ mat . transparent = false ;
907+ mat . opacity = 1.0 ;
908+ mat . depthWrite = true ;
909+ mat . depthTest = true ;
910+ mat . side = THREE . FrontSide ;
911+ mat . color = new THREE . Color ( 0xffffff ) ;
912+
913+ // Slight depth bias helps when solar skin is close to body geometry.
914+ mat . polygonOffset = true ;
915+ mat . polygonOffsetFactor = - 0.5 ;
916+ mat . polygonOffsetUnits = - 0.5 ;
917+
918+ if ( typeof mat . roughness === 'number' ) {
919+ mat . roughness = 0.45 ;
920+ }
921+ if ( typeof mat . metalness === 'number' ) {
922+ mat . metalness = 0.0 ;
923+ }
924+ if ( mat . map ) {
925+ const maxAniso = renderer . capabilities . getMaxAnisotropy ? renderer . capabilities . getMaxAnisotropy ( ) : 1 ;
926+ mat . map . anisotropy = Math . max ( 1 , Math . min ( 8 , maxAniso ) ) ;
927+ if ( 'encoding' in mat . map ) {
928+ mat . map . encoding = THREE . sRGBEncoding ;
929+ }
930+ if ( typeof mat . emissive !== 'undefined' ) {
931+ mat . emissive = new THREE . Color ( 0x151515 ) ;
932+ mat . emissiveMap = mat . map ;
933+ }
934+ mat . map . needsUpdate = true ;
935+ }
936+ node . renderOrder = 2 ;
937+ console . log ( 'Adjusted solar panel material:' , node . name || '(unnamed mesh)' ) ;
938+ }
939+
940+ // Brighten aeroshell/body side surfaces so white accents are visible in showroom lighting.
941+ const isBodySide = matName . includes ( 'aeroshell_body' ) || ( nodeName . includes ( 'aeroshell' ) && ! isSolar ) ;
942+ if ( isBodySide ) {
943+ if ( mat . color && mat . color . setRGB ) {
944+ mat . color . setRGB ( 0.9 , 0.9 , 0.92 ) ;
945+ }
946+ if ( typeof mat . roughness === 'number' ) {
947+ mat . roughness = 0.35 ;
948+ }
949+ if ( typeof mat . metalness === 'number' ) {
950+ mat . metalness = 0.0 ;
951+ }
952+ if ( typeof mat . emissive !== 'undefined' ) {
953+ mat . emissive = new THREE . Color ( 0x0f0f10 ) ;
954+ }
955+ console . log ( 'Brightened body-side material:' , node . name || '(unnamed mesh)' ) ;
956+ }
957+
958+ mat . needsUpdate = true ;
959+ return mat ;
960+ } ) ;
961+
962+ node . material = Array . isArray ( node . material ) ? adjustedMaterials : adjustedMaterials [ 0 ] ;
820963 }
821964 // Find wheels by name (depends on the model structure)
822965 const nodeName = node . name . toLowerCase ( ) ;
@@ -835,6 +978,12 @@ const create3DEnvironment = () => {
835978 } ,
836979 ( error ) => {
837980 console . error ( '❌ Error loading model:' , error ) ;
981+ if ( window . location . protocol === 'file:' ) {
982+ console . error ( 'GLB/GLTF assets must be served over HTTP. Open via a local server, not file://' ) ;
983+ showToast ( 'Model load failed: use http://127.0.0.1:8000 (not file://)' ) ;
984+ } else {
985+ showToast ( 'Model load failed, using fallback car' ) ;
986+ }
838987 console . log ( 'Falling back to simple car...' ) ;
839988 createSimpleCar ( ) ;
840989 }
0 commit comments