Files
planet/frontend/public/earth/js/cable-batches.js
rayd1o 58671e7bc3
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.74.4
2026-09-13 10:27:00 +08:00

177 lines
6.5 KiB
JavaScript

import * as THREE from "three";
const CABLE_STYLE_TEXTURE_MAX_WIDTH = 1024;
// Keep the original Line/Sprite objects for picking and selection. Only their
// drawing is replaced: every segment and landing point remains in the batch.
function ownBatch(object, disposeExtra = () => {}) {
object.frustumCulled = false;
object.raycast = () => {};
return {
object,
dispose() {
object.parent?.remove(object);
object.geometry.dispose();
object.material.dispose();
disposeExtra();
},
};
}
export function createCableLineBatch(lines) {
if (!lines.length) return null;
const vertexCount = lines.reduce((count, line) =>
count + Math.max(0, line.geometry.attributes.position.count - 1) * 2, 0);
const positions = new Float32Array(vertexCount * 3);
const styleIndices = new Float32Array(vertexCount);
const width = Math.min(lines.length, CABLE_STYLE_TEXTURE_MAX_WIDTH);
const height = Math.ceil(lines.length / width);
const styles = new Float32Array(width * height * 4);
const styleTexture = new THREE.DataTexture(styles, width, height, THREE.RGBAFormat, THREE.FloatType);
styleTexture.needsUpdate = true;
let cursor = 0;
lines.forEach((line, index) => {
const source = line.geometry.attributes.position;
for (let segment = 0; segment < source.count - 1; segment += 1) {
for (const endpoint of [segment, segment + 1]) {
positions[cursor * 3] = source.getX(endpoint);
positions[cursor * 3 + 1] = source.getY(endpoint);
positions[cursor * 3 + 2] = source.getZ(endpoint);
styleIndices[cursor] = index;
cursor += 1;
}
}
line.material.visible = false;
line.updateMatrix();
line.matrixAutoUpdate = false;
});
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("styleIndex", new THREE.BufferAttribute(styleIndices, 1));
const material = new THREE.ShaderMaterial({
uniforms: {
styles: { value: styleTexture },
styleSize: { value: new THREE.Vector2(width, height) },
},
transparent: true,
depthTest: true,
depthWrite: true,
vertexShader: `
uniform sampler2D styles;
uniform vec2 styleSize;
attribute float styleIndex;
varying vec4 vStyle;
void main() {
vec2 uv = (vec2(mod(styleIndex, styleSize.x), floor(styleIndex / styleSize.x)) + 0.5) / styleSize;
vStyle = texture2D(styles, uv);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec4 vStyle;
void main() {
if (vStyle.a <= 0.0) discard;
gl_FragColor = vStyle;
}
`,
});
material.linewidth = lines[0].material.linewidth;
const object = new THREE.LineSegments(geometry, material);
object.name = "cable-line-batch";
object.renderOrder = lines[0].renderOrder;
object.onBeforeRender = () => {
let changed = false;
lines.forEach((line, index) => {
const { color, opacity } = line.material;
const offset = index * 4;
const alpha = line.visible ? opacity : 0;
if (styles[offset] !== Math.fround(color.r) || styles[offset + 1] !== Math.fround(color.g)
|| styles[offset + 2] !== Math.fround(color.b) || styles[offset + 3] !== Math.fround(alpha)) {
styles[offset] = color.r;
styles[offset + 1] = color.g;
styles[offset + 2] = color.b;
styles[offset + 3] = alpha;
changed = true;
}
});
if (changed) styleTexture.needsUpdate = true;
};
return ownBatch(object, () => styleTexture.dispose());
}
export function createLandingPointBatch(markers, texture) {
if (!markers.length) return null;
const geometry = new THREE.InstancedBufferGeometry();
const quad = new THREE.PlaneGeometry(1, 1);
geometry.index = quad.index;
geometry.attributes.position = quad.attributes.position;
geometry.attributes.uv = quad.attributes.uv;
const centers = new Float32Array(markers.length * 3);
const sizes = new THREE.InstancedBufferAttribute(new Float32Array(markers.length * 2), 2)
.setUsage(THREE.DynamicDrawUsage);
const styles = new THREE.InstancedBufferAttribute(new Float32Array(markers.length * 4), 4)
.setUsage(THREE.DynamicDrawUsage);
markers.forEach((marker, index) => {
marker.position.toArray(centers, index * 3);
marker.material.visible = false;
});
geometry.setAttribute("center", new THREE.InstancedBufferAttribute(centers, 3));
geometry.setAttribute("size", sizes);
geometry.setAttribute("style", styles);
geometry.instanceCount = markers.length;
const material = new THREE.ShaderMaterial({
uniforms: { map: { value: texture } },
transparent: true,
depthTest: false,
depthWrite: false,
vertexShader: `
attribute vec3 center;
attribute vec2 size;
attribute vec4 style;
varying vec2 vUv;
varying vec4 vStyle;
void main() {
vUv = uv;
vStyle = style;
vec2 scale = vec2(length(modelMatrix[0].xyz), length(modelMatrix[1].xyz));
vec4 viewPosition = modelViewMatrix * vec4(center, 1.0);
viewPosition.xy += position.xy * size * scale;
gl_Position = style.a > 0.0 ? projectionMatrix * viewPosition : vec4(2.0, 2.0, 2.0, 1.0);
}
`,
fragmentShader: `
uniform sampler2D map;
varying vec2 vUv;
varying vec4 vStyle;
void main() {
vec4 color = texture2D(map, vUv) * vStyle;
if (color.a < 0.01) discard;
gl_FragColor = color;
}
`,
});
const object = new THREE.Mesh(geometry, material);
object.name = "landing-point-batch";
object.renderOrder = markers[0].renderOrder;
object.onBeforeRender = () => {
let sizeChanged = false;
let styleChanged = false;
markers.forEach((marker, index) => {
const { color, opacity } = marker.material;
const alpha = marker.visible ? opacity : 0;
if (sizes.getX(index) !== Math.fround(marker.scale.x) || sizes.getY(index) !== Math.fround(marker.scale.y)) {
sizes.setXY(index, marker.scale.x, marker.scale.y);
sizeChanged = true;
}
if (styles.getX(index) !== Math.fround(color.r) || styles.getY(index) !== Math.fround(color.g)
|| styles.getZ(index) !== Math.fround(color.b) || styles.getW(index) !== Math.fround(alpha)) {
styles.setXYZW(index, color.r, color.g, color.b, alpha);
styleChanged = true;
}
});
if (sizeChanged) sizes.needsUpdate = true;
if (styleChanged) styles.needsUpdate = true;
};
return ownBatch(object);
}