Skip to content

Commit

Permalink
Merge branch 'feature-isosuface-heatmap' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
tsengyushiang committed Aug 11, 2024
2 parents b77b5dd + 429a867 commit e786194
Show file tree
Hide file tree
Showing 15 changed files with 755 additions and 192 deletions.
28 changes: 26 additions & 2 deletions packages/react-coverage-heatmap/src/components/Renderer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const Renderer = ({
texture,
textCoordScale,
textCoordSoffset,
isPointcloud,
isIsoSurface,
isoValue,
isHeatmapColor,
isSignalIndex,
signalIntensities,
signals,
Expand All @@ -16,8 +20,28 @@ const Renderer = ({
const canvasRef = useRef(null);

useEffect(() => {
ThreeApp.setTexture(texture, textCoordScale, textCoordSoffset);
}, [texture, textCoordScale, textCoordSoffset]);
ThreeApp.setTexture(undefined, textCoordScale, textCoordSoffset);
}, [textCoordScale, textCoordSoffset]);

useEffect(() => {
ThreeApp.setTexture(texture);
}, [texture]);

useEffect(() => {
ThreeApp.setIsPointcloud(isPointcloud);
}, [isPointcloud]);

useEffect(() => {
ThreeApp.setIsIsoSurface(isIsoSurface);
}, [isIsoSurface]);

useEffect(() => {
ThreeApp.setIsoValue(isoValue);
}, [isoValue]);

useEffect(() => {
ThreeApp.setIsHeatmapColor(isHeatmapColor);
}, [isHeatmapColor]);

useEffect(() => {
ThreeApp.setIsSignalIndex(isSignalIndex);
Expand Down
147 changes: 147 additions & 0 deletions packages/three-coverage-heatmap/src/IsoSurface/UniformSampler3D.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as THREE from "three";
import HeatmapTextureMaterial from "../Material/HeatmapTextureMaterial";

export default class UniformSampler3D {
constructor(samplesY, samplesXZ, scale) {
this._samplesXZ = samplesXZ;
this._texturesPerAxis = Math.ceil(Math.sqrt(samplesY));
const offset = [-scale[0] / 2, 0, -scale[2] / 2];
this._renderMesh = this._createRenderMesh(samplesY, scale, offset);
this._points = this._createVisulizePoints(
samplesY,
samplesXZ,
scale,
offset
);

const resolution = this._texturesPerAxis * samplesXZ;
this._renderTarget = new THREE.WebGLRenderTarget(resolution, resolution);

this.object = new THREE.Group();
this.object.add(this._points);
}

_createVisulizePoints(samplesY, samplesXZ, scale, offset) {
const geometry = new THREE.BufferGeometry();

const vertices = ((size, scale, offset) =>
new Array(size[1])
.fill(0)
.flatMap((_, y) =>
new Array(size[2] * size[0])
.fill(0)
.map((_, index) => [
index % size[0],
y,
Math.floor(index / size[0]),
])
)
.map((data) => data.map((value, index) => value / size[index]))
.map((data) =>
data.map((v, index) => v * scale[index] + offset[index])
))([samplesXZ, samplesY, samplesXZ], scale, offset);

geometry.setAttribute(
"position",
new THREE.BufferAttribute(new Float32Array(vertices.flat()), 3)
);
const material = new THREE.PointsMaterial({ vertexColors: true });
material.size = 0.1;
const points = new THREE.Points(geometry, material);

return points;
}

_createRenderMesh(samplesY, scale, offset) {
const coords = new Array(samplesY).fill(0).map((_, index) => {
const y = index / samplesY;
return [
[1, y, 1],
[0, y, 1],
[1, y, 0],
[1, y, 0],
[0, y, 1],
[0, y, 0],
[1, y, 1],
[1, y, 0],
[0, y, 1],
[1, y, 0],
[0, y, 0],
[0, y, 1],
];
});

const vertices = coords
.flat()
.flatMap((data) =>
data.map((v, index) => v * scale[index] + offset[index])
);

const texturesPerRow = Math.ceil(Math.sqrt(samplesY));
const layerSize = 1 / texturesPerRow;
const uvs = coords.flatMap((data, index) => {
const xOffset = index % texturesPerRow;
const yOffset = Math.floor(index / texturesPerRow);
const uv = data.flatMap(([x, _, z]) => [
x / texturesPerRow + xOffset * layerSize,
z / texturesPerRow + yOffset * layerSize,
]);
return uv;
});

const geometry = new THREE.BufferGeometry();
const material = new HeatmapTextureMaterial();
geometry.setAttribute(
"position",
new THREE.BufferAttribute(new Float32Array(vertices), 3)
);
geometry.setAttribute(
"uv",
new THREE.BufferAttribute(new Float32Array(uvs), 2)
);

const mesh = new THREE.Mesh(geometry, material);
const scene = new THREE.Scene();
scene.add(mesh);

return { object: scene, material };
}

sample(renderer) {
renderer.setRenderTarget(this._renderTarget);
renderer.render(this._renderMesh.object, new THREE.Camera());
renderer.setRenderTarget(null);

const colors = new Array(this._texturesPerAxis ** 2)
.fill(0)
.reduce((prev, _, index) => {
const pixels = new Uint8Array(4 * this._samplesXZ * this._samplesXZ);
const xOffset = (index % this._texturesPerAxis) * this._samplesXZ;
const yOffset =
Math.floor(index / this._texturesPerAxis) * this._samplesXZ;
renderer.readRenderTargetPixels(
this._renderTarget,
xOffset,
yOffset,
this._samplesXZ,
this._samplesXZ,
pixels
);

return [...prev, ...pixels];
}, []);

this._points.geometry.setAttribute(
"color",
new THREE.BufferAttribute(
new Float32Array(colors.map((color) => color / 255)),
4
)
);
return colors;
}

setUniforms(data) {
this._renderMesh.material.setUniforms(data);
}
}
59 changes: 59 additions & 0 deletions packages/three-coverage-heatmap/src/IsoSurface/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as THREE from "three";
import { MarchingCubes } from "three/addons/objects/MarchingCubes.js";

class IsoSurface extends MarchingCubes {
constructor(samplesY, samplesXZ, scale) {
const material = new THREE.MeshBasicMaterial({
color: "red",
side: THREE.DoubleSide,
opacity: 0.5,
transparent: true,
// depthTest: false,
});
super(samplesXZ, material, true, true, 100000);
this._samplesY = samplesY;
this._samplesXZ = samplesXZ;
this._material = material;

this.position.set(0, scale[1] / 2, 0);
this.scale.set(scale[0] / 2, scale[1] / 2, scale[2] / 2);
}

setIsoValue(value) {
this.isolation = value * 255;

const color = new THREE.Color();
5;
color.setHSL(value, 1, 0.5);
this.material.color = color;
this.update();
}

updateFromColors(colors) {
this.reset();

new Array(this._samplesXZ)
.fill(0)
.flatMap((_, y) =>
new Array(this._samplesXZ ** 2)
.fill(0)
.map((_, xz) => [
xz % this._samplesXZ,
y,
Math.floor(xz / this._samplesXZ),
])
)
.forEach(([x, y, z]) => {
const index =
Math.floor((y / this._samplesXZ) * this._samplesY) *
this._samplesXZ ** 2 +
z * this._samplesXZ +
x;
this.setCell(x, y, z, colors[index * 4]);
});

this.update();
}
}

export default IsoSurface;
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import calculateIntensity from "../heatmap.glsl";
const getFragmentShader = (signalCount, aabbCount, planeCount) => `
uniform vec3 signals[${signalCount}];
uniform float signalIntensities[${signalCount}];
uniform int signalCount;
uniform vec3 aabbs[${aabbCount * 2}];
uniform int aabbCount;
uniform vec3 planes[${planeCount * 2}];
uniform int planeCount;
#define SIGNAL_COUNT ${signalCount}
#define AABB_COUNT ${aabbCount * 2}
#define PLANE_COUNT ${planeCount * 2}
${calculateIntensity}
uniform sampler2D map;
uniform vec2 mapScale;
uniform vec2 mapOffset;
uniform bool isSignalIndex;
uniform bool isHeatmapColor;
varying vec4 world_position;
Expand All @@ -26,98 +27,15 @@ vec3 opacityToHSV(float opacity) {
return hsv2rgb(vec3(hue, 1.0, 1.0));
}
float decay(float distance, float intensity) {
return 1.0 / pow(distance / intensity + 1.0, 2.0);
}
// adapted from intersectCube in https://github.com/evanw/webgl-path-tracing/blob/master/webgl-path-tracing.js
// compute the near and far intersections of the cube (stored in the x and y components) using the slab method
// no intersection means vec.x > vec.y (really tNear > tFar)
vec2 intersectAABB(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 boxMax) {
vec3 tMin = (boxMin - rayOrigin) / rayDir;
vec3 tMax = (boxMax - rayOrigin) / rayDir;
vec3 t1 = min(tMin, tMax);
vec3 t2 = max(tMin, tMax);
float tNear = max(max(t1.x, t1.y), t1.z);
float tFar = min(min(t2.x, t2.y), t2.z);
return vec2(tNear, tFar);
}
float PointInOrOn(vec3 P1, vec3 P2, vec3 A, vec3 B) {
vec3 CP1 = cross(B - A, P1 - A);
vec3 CP2 = cross(B - A, P2 - A);
return step(0.0, dot(CP1, CP2));
}
bool PointInTriangle(vec3 px, vec3 p0, vec3 p1, vec3 p2) {
return PointInOrOn(px, p0, p1, p2) * PointInOrOn(px, p1, p2, p0) * PointInOrOn(px, p2, p0, p1) < 1e-3;
}
vec3 IntersectPlane(vec3 rayOrigin, vec3 rayDir, vec3 p0, vec3 p1, vec3 p2) {
vec3 D = rayDir;
vec3 N = cross(p1 - p0, p2 - p0);
vec3 X = rayOrigin + D * dot(p0 - rayOrigin, N) / dot(D, N);
return X;
}
bool pointOnRay(vec3 point, vec3 rayOrigin, vec3 rayDir) {
vec3 intersectionDir = normalize(rayOrigin - point);
return dot(intersectionDir, rayDir) < (1.0 - 1e-3);
}
bool intersect(vec3 origin, vec3 rayDir, vec3 p0, vec3 p1, vec3 p2, float maxDistance) {
vec3 x = IntersectPlane(origin, rayDir, p0, p1, p2);
bool noIntersections = PointInTriangle(x, p0, p1, p2) || !pointOnRay(x, origin, rayDir) || distance(x, origin) > maxDistance - 1e-3;
return !noIntersections;
}
void main() {
float maxSignalIndex = 1.0;
float density = 1e-3;
for (int signalIndex = 0; signalIndex < signalCount; signalIndex++) {
float wallDistance = 0.0;
vec3 signalPosition = signals[signalIndex].xyz;
vec3 rayDir = normalize(world_position.xyz - signalPosition);
float totalDistance = distance(world_position.xyz, signalPosition);
for (int aabbIndex = 0; aabbIndex < aabbCount; aabbIndex++) {
vec2 nearFar = intersectAABB(signalPosition, rayDir, aabbs[2 * aabbIndex], aabbs[2 * aabbIndex + 1]);
bool noIntersections = nearFar.x > nearFar.y || nearFar.x < 0.0 || nearFar.x > totalDistance - 1e-3;
if (noIntersections) {
continue;
}
wallDistance += nearFar.y - nearFar.x;
}
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) {
vec3 min = planes[2 * planeIndex];
vec3 max = planes[2 * planeIndex + 1];
vec3 p0 = min;
vec3 p1 = vec3(max.x, min.y, max.z);
vec3 p2 = max;
vec3 p3 = vec3(min.x, max.y, min.z);
if (!intersect(signalPosition, rayDir, p0, p1, p2, totalDistance) && !intersect(signalPosition, rayDir, p3, p0, p2, totalDistance)) {
continue;
}
wallDistance += 0.15;
}
float wallDecay = wallDistance * 0.2;
float newDensity = decay(totalDistance - wallDistance, signalIntensities[signalIndex]) - wallDecay;
if (newDensity > density) {
density = newDensity;
maxSignalIndex = float(signalIndex) / float(signalCount);
}
vec4 color = texture2D(map, (world_position.xz * mapScale) + mapOffset);
if(!isHeatmapColor){
gl_FragColor = color;
return;
}
vec4 visualizedDensity = vec4(opacityToHSV(isSignalIndex? maxSignalIndex:density), 1.0);
vec4 color = texture2D(map, (world_position.xz * mapScale) + mapOffset);
Result result = getSignalDensity(world_position);
vec4 visualizedDensity = vec4(opacityToHSV(isSignalIndex ? result.maxSignalIndex : result.density), 1.0);
gl_FragColor = mix(color, visualizedDensity, 0.4);
}
`;
Expand Down
Loading

0 comments on commit e786194

Please sign in to comment.