2025-09-05 17:40:07 +05:30
|
|
|
import * as THREE from "three";
|
2025-09-06 12:19:35 +05:30
|
|
|
import { useEffect, useMemo, useRef, useCallback } from "react";
|
2025-09-05 17:40:07 +05:30
|
|
|
import { useThree } from "@react-three/fiber";
|
2025-09-05 17:13:51 +05:30
|
|
|
import { useHeatMapStore } from "../../../store/simulation/useHeatMapStore";
|
2025-09-05 17:40:07 +05:30
|
|
|
import * as CONSTANTS from "../../../types/world/worldConstants";
|
2025-09-06 14:55:53 +05:30
|
|
|
|
2025-09-05 17:13:51 +05:30
|
|
|
|
2025-09-06 12:19:35 +05:30
|
|
|
const RADIUS = 0.0025;
|
|
|
|
|
const OPACITY = 0.8;
|
2025-09-05 17:40:07 +05:30
|
|
|
const GROWTH_RATE = 20.0;
|
|
|
|
|
|
|
|
|
|
const BakedHeatMap = () => {
|
2025-09-06 12:19:35 +05:30
|
|
|
const { bakedPoints } = useHeatMapStore();
|
2025-09-05 17:40:07 +05:30
|
|
|
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
2025-09-06 12:19:35 +05:30
|
|
|
const meshRef = useRef<THREE.Mesh>(null);
|
2025-09-06 13:01:45 +05:30
|
|
|
const { gl } = useThree();
|
2025-09-06 12:19:35 +05:30
|
|
|
|
2025-09-05 17:40:07 +05:30
|
|
|
const height = CONSTANTS.gridConfig.size;
|
|
|
|
|
const width = CONSTANTS.gridConfig.size;
|
|
|
|
|
|
2025-09-06 13:01:45 +05:30
|
|
|
const createPointTexture = useCallback(
|
|
|
|
|
(filteredPoints: typeof bakedPoints) => {
|
|
|
|
|
if (filteredPoints.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
const data = new Float32Array(filteredPoints.length * 4);
|
|
|
|
|
filteredPoints.forEach((p, i) => {
|
|
|
|
|
const index = i * 4;
|
|
|
|
|
data[index] = (p.points.x + width / 2) / width;
|
|
|
|
|
data[index + 1] = (p.points.y + height / 2) / height;
|
2025-09-06 13:15:12 +05:30
|
|
|
data[index + 2] = 0.3;
|
2025-09-06 14:55:53 +05:30
|
|
|
data[index + 3] = 0.0;
|
2025-09-06 13:01:45 +05:30
|
|
|
});
|
|
|
|
|
|
2025-09-06 14:55:53 +05:30
|
|
|
const texture = new THREE.DataTexture(data, filteredPoints.length, 1, THREE.RGBAFormat, THREE.FloatType);
|
2025-09-06 13:01:45 +05:30
|
|
|
texture.needsUpdate = true;
|
|
|
|
|
return texture;
|
|
|
|
|
},
|
|
|
|
|
[width, height]
|
|
|
|
|
);
|
2025-09-05 17:40:07 +05:30
|
|
|
|
|
|
|
|
const uniformsRef = useRef({
|
|
|
|
|
u_points: { value: null as THREE.DataTexture | null },
|
|
|
|
|
u_count: { value: 0 },
|
|
|
|
|
u_radius: { value: RADIUS },
|
|
|
|
|
u_opacity: { value: OPACITY },
|
|
|
|
|
u_growthRate: { value: GROWTH_RATE },
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-06 13:01:45 +05:30
|
|
|
const renderHeatmapToImage = useCallback(
|
|
|
|
|
(type: string) => {
|
|
|
|
|
if (!meshRef.current) return null;
|
|
|
|
|
|
|
|
|
|
const filteredPoints = bakedPoints.filter((p) => p.type === type);
|
|
|
|
|
if (filteredPoints.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
const pointTexture = createPointTexture(filteredPoints);
|
|
|
|
|
if (!pointTexture) return null;
|
|
|
|
|
|
|
|
|
|
uniformsRef.current.u_points.value = pointTexture;
|
|
|
|
|
uniformsRef.current.u_count.value = filteredPoints.length;
|
|
|
|
|
|
2025-09-06 14:55:53 +05:30
|
|
|
const exportCamera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 0.1, 10);
|
2025-09-06 13:01:45 +05:30
|
|
|
exportCamera.position.set(0, 1, 0);
|
|
|
|
|
exportCamera.lookAt(0, 0, 0);
|
|
|
|
|
|
|
|
|
|
const renderTarget = new THREE.WebGLRenderTarget(1024, 1024, {
|
|
|
|
|
format: THREE.RGBAFormat,
|
|
|
|
|
type: THREE.UnsignedByteType,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const tempScene = new THREE.Scene();
|
|
|
|
|
tempScene.add(meshRef.current);
|
|
|
|
|
|
|
|
|
|
gl.setRenderTarget(renderTarget);
|
|
|
|
|
gl.render(tempScene, exportCamera);
|
|
|
|
|
gl.setRenderTarget(null);
|
|
|
|
|
|
|
|
|
|
const pixels = new Uint8Array(1024 * 1024 * 4);
|
|
|
|
|
gl.readRenderTargetPixels(renderTarget, 0, 0, 1024, 1024, pixels);
|
|
|
|
|
|
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
|
canvas.width = 1024;
|
|
|
|
|
canvas.height = 1024;
|
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
|
|
|
|
|
|
if (ctx) {
|
|
|
|
|
const imageData = ctx.createImageData(1024, 1024);
|
|
|
|
|
imageData.data.set(pixels);
|
|
|
|
|
ctx.putImageData(imageData, 0, 0);
|
|
|
|
|
return canvas.toDataURL("image/png");
|
|
|
|
|
}
|
2025-09-06 13:15:12 +05:30
|
|
|
|
2025-09-06 13:01:45 +05:30
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
[gl, width, height, bakedPoints, createPointTexture]
|
|
|
|
|
);
|
2025-09-05 17:40:07 +05:30
|
|
|
|
2025-09-06 13:15:12 +05:30
|
|
|
const downloadImage = (base64: string, filename: string) => {
|
|
|
|
|
const link = document.createElement("a");
|
|
|
|
|
link.href = base64;
|
|
|
|
|
link.download = filename;
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
link.click();
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-06 12:19:35 +05:30
|
|
|
const exportHeatmapAsPNG = useCallback(() => {
|
2025-09-06 13:01:45 +05:30
|
|
|
const types = ["human", "vehicle"];
|
|
|
|
|
|
|
|
|
|
const result = types.map((type) => {
|
|
|
|
|
const image = renderHeatmapToImage(type);
|
2025-09-06 13:15:12 +05:30
|
|
|
if (image) {
|
|
|
|
|
downloadImage(image, `${type}-heatmap.png`);
|
|
|
|
|
}
|
|
|
|
|
return { type, image };
|
2025-09-06 12:19:35 +05:30
|
|
|
});
|
2025-09-05 17:40:07 +05:30
|
|
|
|
2025-09-06 13:01:45 +05:30
|
|
|
console.log("Exported Heatmaps:", result);
|
|
|
|
|
return result;
|
|
|
|
|
}, [renderHeatmapToImage]);
|
|
|
|
|
|
|
|
|
|
const pointTexture = useMemo(() => createPointTexture(bakedPoints), [bakedPoints, createPointTexture]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
uniformsRef.current.u_points.value = pointTexture;
|
|
|
|
|
uniformsRef.current.u_count.value = bakedPoints.length;
|
|
|
|
|
}, [pointTexture, bakedPoints.length]);
|
2025-09-05 17:40:07 +05:30
|
|
|
|
2025-09-06 12:19:35 +05:30
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<mesh ref={meshRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0.025, 0]}>
|
|
|
|
|
<planeGeometry args={[width, height]} />
|
|
|
|
|
<shaderMaterial
|
|
|
|
|
ref={materialRef}
|
|
|
|
|
transparent
|
|
|
|
|
depthWrite={false}
|
|
|
|
|
blending={THREE.AdditiveBlending}
|
|
|
|
|
uniforms={uniformsRef.current}
|
|
|
|
|
side={THREE.DoubleSide}
|
|
|
|
|
vertexShader={`
|
|
|
|
|
varying vec2 vUv;
|
|
|
|
|
void main() {
|
|
|
|
|
vUv = uv;
|
|
|
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
|
|
|
}
|
|
|
|
|
`}
|
|
|
|
|
fragmentShader={`
|
|
|
|
|
uniform sampler2D u_points;
|
|
|
|
|
precision highp float;
|
|
|
|
|
uniform int u_count;
|
|
|
|
|
uniform float u_radius;
|
|
|
|
|
uniform float u_opacity;
|
|
|
|
|
uniform float u_growthRate;
|
|
|
|
|
varying vec2 vUv;
|
|
|
|
|
|
|
|
|
|
float gauss(float dist, float radius) {
|
|
|
|
|
return exp(-pow(dist / radius, 2.0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
float intensity = 0.0;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 10000; i++) {
|
|
|
|
|
if (i >= u_count) break;
|
|
|
|
|
float fi = float(i) + 0.5;
|
|
|
|
|
float u = fi / float(u_count);
|
|
|
|
|
|
|
|
|
|
vec4 point = texture2D(u_points, vec2(u, 0.5));
|
|
|
|
|
vec2 pos = point.rg;
|
|
|
|
|
float strength = point.b;
|
|
|
|
|
|
|
|
|
|
float d = distance(vUv, pos);
|
|
|
|
|
intensity += strength * gauss(d, u_radius);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float normalized = clamp(intensity / max(u_growthRate, 0.0001), 0.0, 1.0);
|
|
|
|
|
|
|
|
|
|
vec3 color = vec3(0.0);
|
|
|
|
|
if (normalized < 0.33) {
|
|
|
|
|
color = mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), normalized / 0.33);
|
|
|
|
|
} else if (normalized < 0.66) {
|
|
|
|
|
color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0), (normalized - 0.33) / 0.33);
|
|
|
|
|
} else {
|
|
|
|
|
color = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (normalized - 0.66) / 0.34);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gl_FragColor = vec4(color, normalized * u_opacity);
|
|
|
|
|
}
|
|
|
|
|
`}
|
|
|
|
|
/>
|
|
|
|
|
</mesh>
|
|
|
|
|
</>
|
2025-09-05 17:40:07 +05:30
|
|
|
);
|
|
|
|
|
};
|
2025-09-05 17:13:51 +05:30
|
|
|
|
|
|
|
|
export default BakedHeatMap;
|