sweet progress
This commit is contained in:
@@ -4,6 +4,40 @@ Convert LAS lidar files to .mound binary format for Three.js rendering.
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python las_to_mound.py input.las output.mound
|
python las_to_mound.py input.las output.mound
|
||||||
|
|
||||||
|
.mound Binary Format Specification
|
||||||
|
==================================
|
||||||
|
|
||||||
|
Header (64 bytes):
|
||||||
|
Offset Size Type Description
|
||||||
|
------ ---- ---------- -----------
|
||||||
|
0 4 char[4] Magic number: 'LIDR'
|
||||||
|
4 4 uint32 Version number (currently 1)
|
||||||
|
8 4 uint32 Point count (number of vertices)
|
||||||
|
12 4 uint32 Triangle count (number of triangles)
|
||||||
|
16 4 float32 Min X coordinate
|
||||||
|
20 4 float32 Min Y coordinate
|
||||||
|
24 4 float32 Min Z coordinate
|
||||||
|
28 4 float32 Max X coordinate
|
||||||
|
32 4 float32 Max Y coordinate
|
||||||
|
36 4 float32 Max Z coordinate
|
||||||
|
40 24 bytes Reserved (padding to 64 bytes)
|
||||||
|
|
||||||
|
Vertex Data (point_count * 12 bytes):
|
||||||
|
Series of vertices in XYZ float32 triplets.
|
||||||
|
Total size: point_count * 3 * 4 bytes
|
||||||
|
|
||||||
|
Index Data (triangle_count * 12 bytes):
|
||||||
|
Series of triangle indices (3 uint32 per triangle).
|
||||||
|
Each index references a vertex in the vertex data.
|
||||||
|
Total size: triangle_count * 3 * 4 bytes
|
||||||
|
|
||||||
|
Example layout for 100 points and 50 triangles:
|
||||||
|
Bytes 0-63: Header
|
||||||
|
Bytes 64-1263: Vertex data (100 * 12)
|
||||||
|
Bytes 1264-1863: Index data (50 * 12)
|
||||||
|
Total file size: 1864 bytes
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
210
ui/src/App1.vue
Normal file
210
ui/src/App1.vue
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<canvas ref="canvas" class="canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App1',
|
||||||
|
setup() {
|
||||||
|
const canvas = ref(null);
|
||||||
|
let scene, camera, renderer, controls;
|
||||||
|
|
||||||
|
const parseMoundFile = async (url) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
const magic = String.fromCharCode(
|
||||||
|
view.getUint8(offset++),
|
||||||
|
view.getUint8(offset++),
|
||||||
|
view.getUint8(offset++),
|
||||||
|
view.getUint8(offset++)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (magic !== 'LIDR') {
|
||||||
|
throw new Error('Invalid .mound file');
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = view.getUint32(offset, true); offset += 4;
|
||||||
|
const pointCount = view.getUint32(offset, true); offset += 4;
|
||||||
|
const triangleCount = view.getUint32(offset, true); offset += 4;
|
||||||
|
const minX = view.getFloat32(offset, true); offset += 4;
|
||||||
|
const minY = view.getFloat32(offset, true); offset += 4;
|
||||||
|
const minZ = view.getFloat32(offset, true); offset += 4;
|
||||||
|
const maxX = view.getFloat32(offset, true); offset += 4;
|
||||||
|
const maxY = view.getFloat32(offset, true); offset += 4;
|
||||||
|
const maxZ = view.getFloat32(offset, true); offset += 4;
|
||||||
|
offset += 24; // Skip reserved
|
||||||
|
|
||||||
|
const positions = new Float32Array(buffer, offset, pointCount * 3);
|
||||||
|
offset += pointCount * 3 * 4;
|
||||||
|
|
||||||
|
const indices = new Uint32Array(buffer, offset, triangleCount * 3);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pointCount,
|
||||||
|
triangleCount,
|
||||||
|
bounds: { minX, minY, minZ, maxX, maxY, maxZ },
|
||||||
|
positions,
|
||||||
|
indices
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadLidarTile = async () => {
|
||||||
|
try {
|
||||||
|
console.log('Loading BS19820747...');
|
||||||
|
const data = await parseMoundFile('/tiles/BS19820747.mound');
|
||||||
|
|
||||||
|
console.log('Bounds:', data.bounds);
|
||||||
|
console.log('Points:', data.pointCount);
|
||||||
|
console.log('Triangles:', data.triangleCount);
|
||||||
|
|
||||||
|
const geometry = new THREE.BufferGeometry();
|
||||||
|
|
||||||
|
// Calculate center and scale first
|
||||||
|
const centerX = (data.bounds.minX + data.bounds.maxX) / 2;
|
||||||
|
const centerY = (data.bounds.minY + data.bounds.maxY) / 2;
|
||||||
|
const centerZ = (data.bounds.minZ + data.bounds.maxZ) / 2;
|
||||||
|
|
||||||
|
const spanX = data.bounds.maxX - data.bounds.minX;
|
||||||
|
const spanY = data.bounds.maxY - data.bounds.minY;
|
||||||
|
const spanZ = data.bounds.maxZ - data.bounds.minZ;
|
||||||
|
const maxSpan = Math.max(spanX, spanY);
|
||||||
|
|
||||||
|
console.log('Center:', centerX, centerY, centerZ);
|
||||||
|
console.log('Span X:', spanX, 'Y:', spanY, 'Z:', spanZ);
|
||||||
|
console.log('Max span:', maxSpan);
|
||||||
|
|
||||||
|
// Transform vertices: center and scale
|
||||||
|
const normalizeScale = 10 / maxSpan;
|
||||||
|
const zScale = normalizeScale * (maxSpan * 0.1) / spanZ;
|
||||||
|
|
||||||
|
console.log('Normalize scale:', normalizeScale);
|
||||||
|
console.log('Z scale:', zScale);
|
||||||
|
|
||||||
|
// Create new position array with transformed coordinates
|
||||||
|
const transformedPositions = new Float32Array(data.positions.length);
|
||||||
|
for (let i = 0; i < data.positions.length; i += 3) {
|
||||||
|
transformedPositions[i] = (data.positions[i] - centerX) * normalizeScale; // X
|
||||||
|
transformedPositions[i + 1] = (data.positions[i + 1] - centerY) * normalizeScale; // Y
|
||||||
|
transformedPositions[i + 2] = (data.positions[i + 2] - centerZ) * zScale; // Z
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.setAttribute('position', new THREE.BufferAttribute(transformedPositions, 3));
|
||||||
|
geometry.setIndex(new THREE.BufferAttribute(data.indices, 1));
|
||||||
|
geometry.computeVertexNormals();
|
||||||
|
|
||||||
|
console.log('First few transformed positions:', transformedPositions.slice(0, 15));
|
||||||
|
|
||||||
|
const material = new THREE.MeshLambertMaterial({
|
||||||
|
color: 0x8B7355,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
wireframe: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const mesh = new THREE.Mesh(geometry, material);
|
||||||
|
scene.add(mesh);
|
||||||
|
|
||||||
|
console.log('Mesh added to scene at origin');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load tile:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Scene
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0x87CEEB);
|
||||||
|
|
||||||
|
// Camera
|
||||||
|
camera = new THREE.PerspectiveCamera(
|
||||||
|
75,
|
||||||
|
window.innerWidth / window.innerHeight,
|
||||||
|
0.1,
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
camera.position.set(0, -15, 10);
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
|
// Renderer
|
||||||
|
renderer = new THREE.WebGLRenderer({
|
||||||
|
canvas: canvas.value,
|
||||||
|
antialias: true
|
||||||
|
});
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
|
||||||
|
// OrbitControls for pan, orbit, zoom
|
||||||
|
controls = new OrbitControls(camera, canvas.value);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.05;
|
||||||
|
controls.target.set(0, 0, 0);
|
||||||
|
|
||||||
|
// Lights for the terrain
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
directionalLight.position.set(5, 5, 10);
|
||||||
|
scene.add(directionalLight);
|
||||||
|
|
||||||
|
// Axes helper
|
||||||
|
const axesHelper = new THREE.AxesHelper(15);
|
||||||
|
scene.add(axesHelper);
|
||||||
|
|
||||||
|
// Load lidar
|
||||||
|
loadLidarTile();
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
const animate = () => {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
controls.update();
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Handle resize
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Three.js initialized');
|
||||||
|
console.log('Scene:', scene);
|
||||||
|
console.log('Camera:', camera);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { canvas };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
398
ui/src/App2.vue
Normal file
398
ui/src/App2.vue
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<canvas ref="canvas" class="canvas"></canvas>
|
||||||
|
|
||||||
|
<div class="control-panel">
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Light Azimuth (E-W)</label>
|
||||||
|
<input
|
||||||
|
v-model.number="controls_state.azimuth"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="360"
|
||||||
|
step="1"
|
||||||
|
@input="onControlChange"
|
||||||
|
/>
|
||||||
|
<span class="value">{{ Math.round(controls_state.azimuth) }}°</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Light Altitude (N-S)</label>
|
||||||
|
<input
|
||||||
|
v-model.number="controls_state.altitude"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="90"
|
||||||
|
step="1"
|
||||||
|
@input="onControlChange"
|
||||||
|
/>
|
||||||
|
<span class="value">{{ Math.round(controls_state.altitude) }}°</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Light Intensity</label>
|
||||||
|
<input
|
||||||
|
v-model.number="controls_state.intensity"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="2"
|
||||||
|
step="0.1"
|
||||||
|
@input="onControlChange"
|
||||||
|
/>
|
||||||
|
<span class="value">{{ controls_state.intensity.toFixed(1) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Height Exaggeration</label>
|
||||||
|
<input
|
||||||
|
v-model.number="controls_state.heightScale"
|
||||||
|
type="range"
|
||||||
|
min="0.1"
|
||||||
|
max="10"
|
||||||
|
step="0.1"
|
||||||
|
@input="onControlChange"
|
||||||
|
/>
|
||||||
|
<span class="value">{{ controls_state.heightScale.toFixed(1) }}x</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App1',
|
||||||
|
setup() {
|
||||||
|
const canvas = ref(null);
|
||||||
|
let scene, camera, renderer, threeControls, mesh, directionalLight;
|
||||||
|
|
||||||
|
const controls_state = ref({
|
||||||
|
azimuth: 45,
|
||||||
|
altitude: 45,
|
||||||
|
intensity: 0.8,
|
||||||
|
heightScale: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
let geometryCache = null;
|
||||||
|
let centerX, centerY, centerZ, spanZ, normalizeScale;
|
||||||
|
|
||||||
|
const parseMoundFile = async (url) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
const magic = String.fromCharCode(
|
||||||
|
view.getUint8(offset++),
|
||||||
|
view.getUint8(offset++),
|
||||||
|
view.getUint8(offset++),
|
||||||
|
view.getUint8(offset++)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (magic !== 'LIDR') {
|
||||||
|
throw new Error('Invalid .mound file');
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = view.getUint32(offset, true); offset += 4;
|
||||||
|
const pointCount = view.getUint32(offset, true); offset += 4;
|
||||||
|
const triangleCount = view.getUint32(offset, true); offset += 4;
|
||||||
|
const minX = view.getFloat32(offset, true); offset += 4;
|
||||||
|
const minY = view.getFloat32(offset, true); offset += 4;
|
||||||
|
const minZ = view.getFloat32(offset, true); offset += 4;
|
||||||
|
const maxX = view.getFloat32(offset, true); offset += 4;
|
||||||
|
const maxY = view.getFloat32(offset, true); offset += 4;
|
||||||
|
const maxZ = view.getFloat32(offset, true); offset += 4;
|
||||||
|
offset += 24; // Skip reserved
|
||||||
|
|
||||||
|
const positions = new Float32Array(buffer, offset, pointCount * 3);
|
||||||
|
offset += pointCount * 3 * 4;
|
||||||
|
|
||||||
|
const indices = new Uint32Array(buffer, offset, triangleCount * 3);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pointCount,
|
||||||
|
triangleCount,
|
||||||
|
bounds: { minX, minY, minZ, maxX, maxY, maxZ },
|
||||||
|
positions,
|
||||||
|
indices
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadLidarTile = async () => {
|
||||||
|
try {
|
||||||
|
console.log('Loading BS19820747...');
|
||||||
|
const data = await parseMoundFile('/tiles/BS19820747.mound');
|
||||||
|
|
||||||
|
console.log('Bounds:', data.bounds);
|
||||||
|
console.log('Points:', data.pointCount);
|
||||||
|
console.log('Triangles:', data.triangleCount);
|
||||||
|
|
||||||
|
const geometry = new THREE.BufferGeometry();
|
||||||
|
|
||||||
|
// Calculate center and scale first
|
||||||
|
centerX = (data.bounds.minX + data.bounds.maxX) / 2;
|
||||||
|
centerY = (data.bounds.minY + data.bounds.maxY) / 2;
|
||||||
|
centerZ = (data.bounds.minZ + data.bounds.maxZ) / 2;
|
||||||
|
|
||||||
|
const spanX = data.bounds.maxX - data.bounds.minX;
|
||||||
|
const spanY = data.bounds.maxY - data.bounds.minY;
|
||||||
|
spanZ = data.bounds.maxZ - data.bounds.minZ;
|
||||||
|
const maxSpan = Math.max(spanX, spanY);
|
||||||
|
|
||||||
|
console.log('Center:', centerX, centerY, centerZ);
|
||||||
|
console.log('Span X:', spanX, 'Y:', spanY, 'Z:', spanZ);
|
||||||
|
console.log('Max span:', maxSpan);
|
||||||
|
|
||||||
|
// Transform vertices: center and scale
|
||||||
|
normalizeScale = 10 / maxSpan;
|
||||||
|
const zScale = normalizeScale * (maxSpan * 0.1) / spanZ;
|
||||||
|
|
||||||
|
console.log('Normalize scale:', normalizeScale);
|
||||||
|
console.log('Z scale:', zScale);
|
||||||
|
|
||||||
|
// Create new position array with transformed coordinates
|
||||||
|
const transformedPositions = new Float32Array(data.positions.length);
|
||||||
|
for (let i = 0; i < data.positions.length; i += 3) {
|
||||||
|
transformedPositions[i] = (data.positions[i] - centerX) * normalizeScale; // X
|
||||||
|
transformedPositions[i + 1] = (data.positions[i + 1] - centerY) * normalizeScale; // Y
|
||||||
|
transformedPositions[i + 2] = (data.positions[i + 2] - centerZ) * zScale; // Z
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.setAttribute('position', new THREE.BufferAttribute(transformedPositions, 3));
|
||||||
|
geometry.setIndex(new THREE.BufferAttribute(data.indices, 1));
|
||||||
|
geometry.computeVertexNormals();
|
||||||
|
|
||||||
|
geometryCache = { geometry, baseZ: new Float32Array(data.positions.map((v, i) => i % 3 === 2 ? (v - centerZ) * zScale : 0)), zScale };
|
||||||
|
|
||||||
|
console.log('First few transformed positions:', transformedPositions.slice(0, 15));
|
||||||
|
|
||||||
|
const material = new THREE.MeshLambertMaterial({
|
||||||
|
color: 0x8B7355,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
wireframe: false
|
||||||
|
});
|
||||||
|
|
||||||
|
mesh = new THREE.Mesh(geometry, material);
|
||||||
|
scene.add(mesh);
|
||||||
|
|
||||||
|
// Apply initial height scale
|
||||||
|
updateHeightScale();
|
||||||
|
|
||||||
|
console.log('Mesh added to scene at origin');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load tile:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateHeightScale = () => {
|
||||||
|
if (!geometryCache) return;
|
||||||
|
|
||||||
|
const positions = geometryCache.geometry.attributes.position.array;
|
||||||
|
const exaggeration = controls_state.value.heightScale;
|
||||||
|
|
||||||
|
// Only every 3rd value (Z coordinate)
|
||||||
|
for (let i = 2; i < positions.length; i += 3) {
|
||||||
|
positions[i] = geometryCache.baseZ[i] * exaggeration;
|
||||||
|
}
|
||||||
|
|
||||||
|
geometryCache.geometry.attributes.position.needsUpdate = true;
|
||||||
|
geometryCache.geometry.computeVertexNormals();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLightPosition = () => {
|
||||||
|
if (!directionalLight) return;
|
||||||
|
|
||||||
|
const azimuth = (controls_state.value.azimuth * Math.PI) / 180;
|
||||||
|
const altitude = (controls_state.value.altitude * Math.PI) / 180;
|
||||||
|
|
||||||
|
const distance = 20;
|
||||||
|
const x = Math.cos(azimuth) * Math.cos(altitude) * distance;
|
||||||
|
const y = Math.sin(azimuth) * Math.cos(altitude) * distance;
|
||||||
|
const z = Math.sin(altitude) * distance;
|
||||||
|
|
||||||
|
directionalLight.position.set(x, y, z);
|
||||||
|
directionalLight.intensity = controls_state.value.intensity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onControlChange = () => {
|
||||||
|
updateLightPosition();
|
||||||
|
updateHeightScale();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Scene
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0x87CEEB);
|
||||||
|
|
||||||
|
// Camera
|
||||||
|
camera = new THREE.PerspectiveCamera(
|
||||||
|
75,
|
||||||
|
window.innerWidth / window.innerHeight,
|
||||||
|
0.1,
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
camera.position.set(0, -15, 10);
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
|
// Renderer
|
||||||
|
renderer = new THREE.WebGLRenderer({
|
||||||
|
canvas: canvas.value,
|
||||||
|
antialias: true
|
||||||
|
});
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
|
||||||
|
// OrbitControls for pan, orbit, zoom
|
||||||
|
threeControls = new OrbitControls(camera, canvas.value);
|
||||||
|
threeControls.enableDamping = true;
|
||||||
|
threeControls.dampingFactor = 0.05;
|
||||||
|
threeControls.target.set(0, 0, 0);
|
||||||
|
|
||||||
|
// Lights for the terrain
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
|
||||||
|
directionalLight = new THREE.DirectionalLight(0xffffff, controls_state.value.intensity);
|
||||||
|
scene.add(directionalLight);
|
||||||
|
|
||||||
|
// Set initial light position
|
||||||
|
updateLightPosition();
|
||||||
|
|
||||||
|
// Axes helper
|
||||||
|
const axesHelper = new THREE.AxesHelper(15);
|
||||||
|
scene.add(axesHelper);
|
||||||
|
|
||||||
|
// Load lidar
|
||||||
|
loadLidarTile();
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
const animate = () => {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
threeControls.update();
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Handle resize
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Three.js initialized');
|
||||||
|
console.log('Scene:', scene);
|
||||||
|
console.log('Camera:', camera);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { canvas, controls_state, onControlChange };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
width: 280px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #4A9EFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group input[type="range"]::-webkit-slider-thumb:hover {
|
||||||
|
background: #5BADFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #4A9EFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group input[type="range"]::-moz-range-thumb:hover {
|
||||||
|
background: #5BADFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user