diff --git a/VERSION b/VERSION index c3c6b0da..909f3ee5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.40.2 \ No newline at end of file +0.40.3 \ No newline at end of file diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 179eef05..9a9eaa8b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,16 @@ This project follows the repository versioning rule: ## [0.39.0] — 2026-04-24 +## [0.40.3] — 2026-04-25 + +### 🔧 Improvements +- 卫星点云升级为自定义 ShaderMaterial,支持 per-point alpha 控制,锁定/悬停卫星从点云中精确隐藏 +- 修复锁定环与自发光选中标记的 depthTest 错误(false → true),消除远端渲染穿透 artifact +- 新增锁定环悬停态缩放与线宽(LOCKED_RING_HOVER_SCALE / LOCKED_RING_HOVER_LINE_WIDTH) +- 修复 updateLockedDotWorldTransform / updateLockedHaloWorldTransform 未强制刷新 matrixWorld 导致的位置漂移 + +--- + ## [0.40.2] — 2026-04-24 ### 🔧 Improvements diff --git a/docs/version-history.md b/docs/version-history.md index 89fb1eb3..18d0b979 100644 --- a/docs/version-history.md +++ b/docs/version-history.md @@ -16,12 +16,13 @@ ## Current Version - `main` 当前主线历史推导到:`0.16.5` -- `dev` 当前开发分支历史推导到:`0.40.2` +- `dev` 当前开发分支历史推导到:`0.40.3` ## Timeline | Version | Type | Branch | Commit | Summary | | --- | --- | --- | --- | --- | +| `0.40.3` | improvement | `dev` | `pending` | 卫星点云升级 ShaderMaterial,修复锁定环 depthTest 与位置漂移,新增悬停态缩放 | | `0.40.2` | improvement | `dev` | `pending` | 卫星点大小随镜头缩放动态调整,调小默认基础尺寸 | | `0.40.1` | improvement | `dev` | `pending` | 卫星选中标记配色跟随图例,修复 footprint 遮蔽卫星渲染问题,修复选中海缆误触发卫星高亮 | | `0.40.0` | feature | `dev` | `pending` | Earth 卫星 footprint 按星座能力分层,Iridium 独立 coverage ring 落地,卫星详情卡补齐覆盖能力与当前显示说明 | diff --git a/frontend/package.json b/frontend/package.json index bd80ad1e..24682574 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "planet-frontend", - "version": "0.40.2", + "version": "0.40.3", "private": true, "packageManager": "bun@1", "dependencies": { diff --git a/frontend/public/earth/js/constants.js b/frontend/public/earth/js/constants.js index 05e1d8f6..efe2a9a8 100644 --- a/frontend/public/earth/js/constants.js +++ b/frontend/public/earth/js/constants.js @@ -283,7 +283,9 @@ export const SATELLITE_CONFIG = { displayAltitudeOffset: 8, frontFacingDotThreshold: 0.015, overlayRenderOrder: 12, - dotSize: 2.8, + dotBaseSize: 2.8, + dotBackdropScale: 1.28, + dotZoomScalePower: 1, ringSize: 0.07, apiPath: '/api/v1/visualization/geo/satellites', breathingSpeed: 0.08, diff --git a/frontend/public/earth/js/satellites.js b/frontend/public/earth/js/satellites.js index 05226f52..e93707e4 100644 --- a/frontend/public/earth/js/satellites.js +++ b/frontend/public/earth/js/satellites.js @@ -81,8 +81,10 @@ const LOCKED_HALO_CORE_PIXEL_RADIUS = 8; const LOCKED_HALO_PIXEL_RADIUS = 24; const FILLED_TEXTURE_RADIUS_RATIO = 0.28; const LOCKED_RING_IDLE_SCALE = 0.68; +const LOCKED_RING_HOVER_SCALE = 1.32; +const LOCKED_RING_HOVER_LINE_WIDTH = 5; +const HOVER_RING_LINE_WIDTH = 3; const LOCKED_RING_IDLE_OPACITY = 0.92; -const DOT_ZOOM_SCALE_POWER = 1; const EARTH_RADIUS_KM = 6378.137; const GROUND_FOOTPRINT_MIN_ELEVATION_DEG = 25; const GROUND_FOOTPRINT_RADIUS_OFFSET = 0.72; @@ -112,6 +114,71 @@ const satelliteSunDirection = new THREE.Vector3(1, 0.2, 0.4).normalize(); export let breathingPhase = 0; +function getPointPixelRatio() { + return typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1; +} + +function getSatelliteDotBaseSize() { + return SATELLITE_CONFIG.dotBaseSize; +} + +function getSatelliteDotBackdropSize() { + return getSatelliteDotBaseSize() * SATELLITE_CONFIG.dotBackdropScale; +} + +function getSatelliteDotZoomScale(cameraDistance) { + return Math.pow(CONFIG.defaultCameraZ / Math.max(cameraDistance, 1), SATELLITE_CONFIG.dotZoomScalePower); +} + +function createSatellitePointMaterial({ + texture, + size, + opacity = 1, + useVertexColor = true, + baseColor = 0xffffff, + alphaTest = 0.04, +}) { + return new THREE.ShaderMaterial({ + uniforms: { + pointTexture: { value: texture }, + size: { value: size * getPointPixelRatio() }, + opacity: { value: opacity }, + baseColor: { value: new THREE.Color(baseColor) }, + }, + transparent: true, + depthTest: true, + depthWrite: false, + vertexShader: ` + uniform float size; + uniform vec3 baseColor; + attribute float alpha; + ${useVertexColor ? "attribute vec3 color;" : ""} + varying float vAlpha; + varying vec3 vColor; + + void main() { + vAlpha = alpha; + vColor = ${useVertexColor ? "color" : "baseColor"}; + gl_PointSize = size; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform sampler2D pointTexture; + uniform float opacity; + varying float vAlpha; + varying vec3 vColor; + + void main() { + vec4 texel = texture2D(pointTexture, gl_PointCoord); + float alphaValue = texel.a * opacity * vAlpha; + if (alphaValue < ${alphaTest.toFixed(3)}) discard; + gl_FragColor = vec4(vColor * texel.rgb, alphaValue); + } + `, + }); +} + const SATELLITE_LEGEND_RULES = [ { key: "equatorial", @@ -197,9 +264,22 @@ export function updateBreathingPhase(deltaTime = 16) { export function updateSatellitePointSize() { if (!satellitePoints || !cameraRef) return; const camDist = cameraRef.position.length(); - const zoomScale = Math.pow(CONFIG.defaultCameraZ / Math.max(camDist, 1), DOT_ZOOM_SCALE_POWER); - satellitePoints.material.size = SATELLITE_CONFIG.dotSize * zoomScale; - satelliteBackdropPoints.material.size = SATELLITE_CONFIG.dotSize * 1.28 * zoomScale; + const zoomScale = getSatelliteDotZoomScale(camDist); + const pointPixelRatio = getPointPixelRatio(); + const dotSize = getSatelliteDotBaseSize() * zoomScale; + const backdropSize = getSatelliteDotBackdropSize() * zoomScale; + const shaderDotSize = dotSize * pointPixelRatio; + const shaderBackdropSize = backdropSize * pointPixelRatio; + if (satellitePoints.material.uniforms?.size) { + satellitePoints.material.uniforms.size.value = shaderDotSize; + } else { + satellitePoints.material.size = dotSize; + } + if (satelliteBackdropPoints.material.uniforms?.size) { + satelliteBackdropPoints.material.uniforms.size.value = shaderBackdropSize; + } else { + satelliteBackdropPoints.material.size = backdropSize; + } } function getBreathingPulse(phase) { @@ -338,7 +418,7 @@ function createBackdropDotTexture() { return texture; } -function createRingTexture(innerRadius, outerRadius, color = "#ffffff") { +function createRingTexture(innerRadius, outerRadius, color = "#ffffff", lineWidth = 3) { const size = DOT_TEXTURE_SIZE * 2; const canvas = document.createElement("canvas"); canvas.width = size; @@ -347,7 +427,7 @@ function createRingTexture(innerRadius, outerRadius, color = "#ffffff") { const center = size / 2; ctx.strokeStyle = color; - ctx.lineWidth = 3; + ctx.lineWidth = lineWidth; ctx.beginPath(); ctx.arc(center, center, (innerRadius + outerRadius) / 2, 0, Math.PI * 2); ctx.stroke(); @@ -384,26 +464,21 @@ export function createSatellites(scene, earthObj) { const pointsGeometry = new THREE.BufferGeometry(); const backdropGeometry = new THREE.BufferGeometry(); - const backdropMaterial = new THREE.PointsMaterial({ - size: SATELLITE_CONFIG.dotSize * 1.28, - map: backdropTexture, - color: 0x0b1626, - transparent: true, + const backdropMaterial = createSatellitePointMaterial({ + size: getSatelliteDotBackdropSize(), + texture: backdropTexture, + useVertexColor: false, + baseColor: 0x0b1626, opacity: 0.42, - sizeAttenuation: false, alphaTest: 0.04, - depthWrite: false, }); - const pointsMaterial = new THREE.PointsMaterial({ - size: SATELLITE_CONFIG.dotSize, - map: dotTexture, - vertexColors: true, - transparent: true, + const pointsMaterial = createSatellitePointMaterial({ + size: getSatelliteDotBaseSize(), + texture: dotTexture, + useVertexColor: true, opacity: 0.9, - sizeAttenuation: false, alphaTest: 0.1, - depthWrite: false, }); satelliteBackdropPoints = new THREE.Points(backdropGeometry, backdropMaterial); @@ -492,6 +567,9 @@ function ensureSatelliteCapacity(count) { const previousBackdropPositions = satelliteBackdropPoints.geometry.attributes.position?.array || null; const previousColors = satellitePoints.geometry.attributes.color?.array || null; + const previousPointAlphas = satellitePoints.geometry.attributes.alpha?.array || null; + const previousBackdropAlphas = + satelliteBackdropPoints.geometry.attributes.alpha?.array || null; const previousTrailPositions = satelliteTrails.geometry.attributes.position?.array || null; const previousTrailColors = @@ -502,6 +580,10 @@ function ensureSatelliteCapacity(count) { const positions = new Float32Array(nextCapacity * 3); const backdropPositions = new Float32Array(nextCapacity * 3); const colors = new Float32Array(nextCapacity * 3); + const pointAlphas = new Float32Array(nextCapacity); + const backdropAlphas = new Float32Array(nextCapacity); + pointAlphas.fill(1); + backdropAlphas.fill(1); if (previousPointPositions) { positions.set( previousPointPositions.subarray(0, Math.min(previousPointPositions.length, positions.length)), @@ -518,10 +600,27 @@ function ensureSatelliteCapacity(count) { if (previousColors) { colors.set(previousColors.subarray(0, Math.min(previousColors.length, colors.length))); } + if (previousPointAlphas) { + pointAlphas.set( + previousPointAlphas.subarray(0, Math.min(previousPointAlphas.length, pointAlphas.length)), + ); + } + if (previousBackdropAlphas) { + backdropAlphas.set( + previousBackdropAlphas.subarray( + 0, + Math.min(previousBackdropAlphas.length, backdropAlphas.length), + ), + ); + } satelliteBackdropPoints.geometry.setAttribute( "position", new THREE.BufferAttribute(backdropPositions, 3), ); + satelliteBackdropPoints.geometry.setAttribute( + "alpha", + new THREE.BufferAttribute(backdropAlphas, 1), + ); satelliteBackdropPoints.geometry.setDrawRange( 0, Math.min(previousCapacity, nextCapacity), @@ -534,6 +633,10 @@ function ensureSatelliteCapacity(count) { "color", new THREE.BufferAttribute(colors, 3), ); + satellitePoints.geometry.setAttribute( + "alpha", + new THREE.BufferAttribute(pointAlphas, 1), + ); satellitePoints.geometry.setDrawRange(0, Math.min(previousCapacity, nextCapacity)); const trailPositions = new Float32Array(nextCapacity * TRAIL_LENGTH * 3); @@ -579,6 +682,25 @@ function ensureSatelliteCapacity(count) { satelliteCapacity = nextCapacity; } +function shouldHideSatellitePoint(index) { + return index === hoveredSatelliteIndex || index === lockedSatelliteIndex; +} + +function updateSatellitePointVisibilityAttributes(count = satelliteData.length) { + const pointAlphaAttr = satellitePoints?.geometry?.attributes?.alpha; + const backdropAlphaAttr = satelliteBackdropPoints?.geometry?.attributes?.alpha; + if (!pointAlphaAttr?.array || !backdropAlphaAttr?.array) return; + + const visibleCount = Math.min(count, pointAlphaAttr.array.length, backdropAlphaAttr.array.length); + for (let i = 0; i < visibleCount; i++) { + const alpha = shouldHideSatellitePoint(i) ? 0 : 1; + pointAlphaAttr.array[i] = alpha; + backdropAlphaAttr.array[i] = alpha; + } + pointAlphaAttr.needsUpdate = true; + backdropAlphaAttr.needsUpdate = true; +} + function computeSatellitePosition(satellite, time) { try { const props = satellite.properties; @@ -824,6 +946,8 @@ export function updateSatellitePositions(deltaTime = 0, force = false) { const backdropPositions = satelliteBackdropPoints.geometry.attributes.position.array; const colors = satellitePoints.geometry.attributes.color.array; + const pointAlphas = satellitePoints.geometry.attributes.alpha.array; + const backdropAlphas = satelliteBackdropPoints.geometry.attributes.alpha.array; const trailPositions = satelliteTrails.geometry.attributes.position.array; const trailColors = satelliteTrails.geometry.attributes.color.array; const baseTime = new Date(Date.now() + elapsedMs); @@ -871,6 +995,9 @@ export function updateSatellitePositions(deltaTime = 0, force = false) { colors[i * 3] = r * pointBrightness; colors[i * 3 + 1] = g * pointBrightness; colors[i * 3 + 2] = b * pointBrightness; + const pointAlpha = shouldHideSatellitePoint(i) ? 0 : 1; + pointAlphas[i] = pointAlpha; + backdropAlphas[i] = pointAlpha; const satPosition = satellitePositions[i]; for (let j = 0; j < TRAIL_LENGTH; j++) { @@ -909,6 +1036,8 @@ export function updateSatellitePositions(deltaTime = 0, force = false) { backdropPositions[i * 3] = 0; backdropPositions[i * 3 + 1] = 0; backdropPositions[i * 3 + 2] = 0; + pointAlphas[i] = 0; + backdropAlphas[i] = 0; for (let j = 0; j < TRAIL_LENGTH; j++) { const trailIdx = (i * TRAIL_LENGTH + j) * 3; @@ -920,8 +1049,10 @@ export function updateSatellitePositions(deltaTime = 0, force = false) { satellitePoints.geometry.attributes.position.needsUpdate = true; satellitePoints.geometry.attributes.color.needsUpdate = true; + satellitePoints.geometry.attributes.alpha.needsUpdate = true; satellitePoints.geometry.setDrawRange(0, count); satelliteBackdropPoints.geometry.attributes.position.needsUpdate = true; + satelliteBackdropPoints.geometry.attributes.alpha.needsUpdate = true; satelliteBackdropPoints.geometry.setDrawRange(0, count); satelliteTrails.geometry.attributes.position.needsUpdate = true; @@ -1014,10 +1145,12 @@ export function setSatelliteSunDirection(direction) { export function setLockedSatelliteIndex(index) { lockedSatelliteIndex = index; + updateSatellitePointVisibilityAttributes(); } export function setHoveredSatelliteIndex(index) { hoveredSatelliteIndex = index; + updateSatellitePointVisibilityAttributes(); } function normalizeSatelliteDisplayStyle(nextStyle) { @@ -1190,7 +1323,7 @@ export function isSatelliteFrontFacing(index, camera = cameraRef) { function createLockedHaloMaterial(color = "#ffbf47") { return new THREE.ShaderMaterial({ transparent: true, - depthTest: false, + depthTest: true, depthWrite: false, side: THREE.DoubleSide, uniforms: { @@ -1397,6 +1530,7 @@ function clearLockedSatelliteStyleVisuals() { function updateLockedDotWorldTransform(position) { if (!lockedDotSprite || !position || !earthObjRef) return; + earthObjRef.updateMatrixWorld(true); const worldPosition = position.clone().applyMatrix4(earthObjRef.matrixWorld); lockedDotSprite.position.copy(worldPosition); if (cameraRef) { @@ -1417,6 +1551,7 @@ function updateLockedDotWorldTransform(position) { function updateLockedHaloWorldTransform(position) { if (!position || !earthObjRef || !lockedHaloMesh) return; + earthObjRef.updateMatrixWorld(true); const worldPosition = position.clone().applyMatrix4(earthObjRef.matrixWorld); const viewDirection = cameraRef ? scratchToCamera.subVectors(cameraRef.position, worldPosition).normalize() @@ -1764,7 +1899,7 @@ function showSelfGlowStyle(position, color = "#ffd25a") { color: new THREE.Color(color), transparent: true, opacity: 0.96, - depthTest: false, + depthTest: true, depthWrite: false, side: THREE.DoubleSide, }); @@ -1824,6 +1959,7 @@ function createRingSprite(position, isLocked = false, color = "#ffcc00") { 8, 12, isLocked ? color : "#ffffff", + isLocked ? LOCKED_RING_HOVER_LINE_WIDTH : HOVER_RING_LINE_WIDTH, ); const filledTexture = isLocked ? createFilledCircleTexture(color) @@ -1832,7 +1968,9 @@ function createRingSprite(position, isLocked = false, color = "#ffcc00") { map: ringTexture, transparent: true, opacity: 0.8, - depthTest: false, + depthTest: true, + depthWrite: false, + alphaTest: 0.01, sizeAttenuation: false, }); @@ -1962,7 +2100,7 @@ export function updateLockedRingPosition(position) { 1 + (ringPulse * 2 - 1) * SATELLITE_CONFIG.breathingScaleAmplitude; const markerSize = isHovered - ? SATELLITE_CONFIG.ringSize + ? SATELLITE_CONFIG.ringSize * LOCKED_RING_HOVER_SCALE : SATELLITE_CONFIG.ringSize * LOCKED_RING_IDLE_SCALE; lockedRingSprite.scale.set( markerSize * breathScale, @@ -2026,26 +2164,38 @@ export function setSatelliteRingState(index, state, position) { hoveredSatelliteIndex = index; hideHoverRings(); showHoverRing(position, false); + updateSatellitePointVisibilityAttributes(); break; case "locked": - hoveredSatelliteIndex = null; hideHoverRings(); showHoverRing(position, true); + updateSatellitePointVisibilityAttributes(); break; case "none": hoveredSatelliteIndex = null; hideHoverRings(); hideLockedRing(); + updateSatellitePointVisibilityAttributes(); break; } } function applyDimMaterialState(isDimmed) { if (satellitePoints) { - satellitePoints.material.opacity = isDimmed ? DIMMED_SATELLITE_POINT_OPACITY : 0.9; + const opacity = isDimmed ? DIMMED_SATELLITE_POINT_OPACITY : 0.9; + if (satellitePoints.material.uniforms?.opacity) { + satellitePoints.material.uniforms.opacity.value = opacity; + } else { + satellitePoints.material.opacity = opacity; + } } if (satelliteBackdropPoints) { - satelliteBackdropPoints.material.opacity = isDimmed ? DIMMED_SATELLITE_BACKDROP_OPACITY : 0.42; + const opacity = isDimmed ? DIMMED_SATELLITE_BACKDROP_OPACITY : 0.42; + if (satelliteBackdropPoints.material.uniforms?.opacity) { + satelliteBackdropPoints.material.uniforms.opacity.value = opacity; + } else { + satelliteBackdropPoints.material.opacity = opacity; + } } } @@ -2242,6 +2392,7 @@ export function clearSatelliteData() { if (satellitePoints) { const positionAttr = satellitePoints.geometry.attributes.position; const colorAttr = satellitePoints.geometry.attributes.color; + const alphaAttr = satellitePoints.geometry.attributes.alpha; if (positionAttr?.array) { positionAttr.array.fill(0); positionAttr.needsUpdate = true; @@ -2250,16 +2401,26 @@ export function clearSatelliteData() { colorAttr.array.fill(0); colorAttr.needsUpdate = true; } + if (alphaAttr?.array) { + alphaAttr.array.fill(0); + alphaAttr.needsUpdate = true; + } satellitePoints.geometry.setDrawRange(0, 0); } if (satelliteBackdropPoints) { const backdropPositionAttr = satelliteBackdropPoints.geometry.attributes.position; + const backdropAlphaAttr = + satelliteBackdropPoints.geometry.attributes.alpha; if (backdropPositionAttr?.array) { backdropPositionAttr.array.fill(0); backdropPositionAttr.needsUpdate = true; } + if (backdropAlphaAttr?.array) { + backdropAlphaAttr.array.fill(0); + backdropAlphaAttr.needsUpdate = true; + } satelliteBackdropPoints.geometry.setDrawRange(0, 0); } diff --git a/pyproject.toml b/pyproject.toml index fdffb19f..3ca375e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "planet" -version = "0.40.2" +version = "0.40.3" description = "智能星球计划 - 态势感知系统" requires-python = ">=3.14" dependencies = [ diff --git a/uv.lock b/uv.lock index ee2d1c82..04025565 100644 --- a/uv.lock +++ b/uv.lock @@ -475,7 +475,7 @@ wheels = [ [[package]] name = "planet" -version = "0.40.2" +version = "0.40.3" source = { virtual = "." } dependencies = [ { name = "aiofiles" },