Skip to content

Commit 8d80681

Browse files
authored
Merge pull request #1 from badgerloop-software/add-sunburst
Added Initial model for sunburst
2 parents 7f4733e + aa5dcb1 commit 8d80681

6 files changed

Lines changed: 500 additions & 7 deletions

File tree

carViewer.js

Lines changed: 156 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<link rel="icon" href="data:," />
45
<style>
56
html,
67
body {

sunburst-body/SolarCell_Tile.png

5.76 KB
Loading

sunburst-body/sunburst-body.bin

1.56 MB
Binary file not shown.

sunburst-body/sunburst-body.glb

20.1 MB
Binary file not shown.

0 commit comments

Comments
 (0)