WebGL着色器32位浮点数精度损失问题
WebGL中着色器只支持32位浮点数,而JavaScript浮点数支持64位,所以浮点数上传过程中存在精度损失,会导致抖动问题。本文总结了在JavaScript API GL 三维地图API开发过程中遇到的浮点数精度损失以及处理方案
问题
WebGL浮点数精度最大的问题是就是因为js是64位精度的,js往着色器里面穿的时候只能是32位浮点数,有效数是8位,精度丢失比较严重。
分析
继续尝试发现mapbox中也有类似问题:https://github.com/mapbox/mapbox-gl-js/issues/7268
map.renderEngin.gl.getShaderPrecisionFormat( map.renderEngin.gl.VERTEX_SHADER, map.renderEngin.gl.HIGH_FLOAT )
解决
getDistanceScales() { // {latitude, longitude, zoom, scale, highPrecision = false} let center = this.center; let latitude = center.lat; let longitude = center.lng; let scale = this.zoomScale(this.zoom); let highPrecision = true; // Calculate scale from zoom if not provided scale = scale !== undefined ? scale : this.zoomToScale(zoom); // assert(Number.isFinite(latitude) && Number.isFinite(longitude) && Number.isFinite(scale)); const result = {}; const worldSize = TILE_SIZE * scale; const latCosine = Math.cos(latitude * DEGREES_TO_RADIANS); /** * Number of pixels occupied by one degree longitude around current lat/lon: pixelsPerDegreeX = d(lngLatToWorld([lng, lat])[0])/d(lng) = scale * TILE_SIZE * DEGREES_TO_RADIANS / (2 * PI) pixelsPerDegreeY = d(lngLatToWorld([lng, lat])[1])/d(lat) = -scale * TILE_SIZE * DEGREES_TO_RADIANS / cos(lat * DEGREES_TO_RADIANS) / (2 * PI) */ const pixelsPerDegreeX = worldSize / 360; const pixelsPerDegreeY = pixelsPerDegreeX / latCosine; /** * Number of pixels occupied by one meter around current lat/lon: */ const altPixelsPerMeter = worldSize / EARTH_CIRCUMFERENCE / latCosine; /** * LngLat: longitude -> east and latitude -> north (bottom left) * UTM meter offset: x -> east and y -> north (bottom left) * World space: x -> east and y -> south (top left) * * Y needs to be flipped when converting delta degree/meter to delta pixels */ result.pixelsPerMeter = [altPixelsPerMeter, altPixelsPerMeter, altPixelsPerMeter]; result.metersPerPixel = [1 / altPixelsPerMeter, 1 / altPixelsPerMeter, 1 / altPixelsPerMeter]; result.pixelsPerDegree = [pixelsPerDegreeX, pixelsPerDegreeY, altPixelsPerMeter]; result.degreesPerPixel = [1 / pixelsPerDegreeX, 1 / pixelsPerDegreeY, 1 / altPixelsPerMeter]; /** * Taylor series 2nd order for 1/latCosine f\'(a) * (x - a) = d(1/cos(lat * DEGREES_TO_RADIANS))/d(lat) * dLat = DEGREES_TO_RADIANS * tan(lat * DEGREES_TO_RADIANS) / cos(lat * DEGREES_TO_RADIANS) * dLat */ if (highPrecision) { const latCosine2 = DEGREES_TO_RADIANS * Math.tan(latitude * DEGREES_TO_RADIANS) / latCosine; const pixelsPerDegreeY2 = pixelsPerDegreeX * latCosine2 / 2; const altPixelsPerDegree2 = worldSize / EARTH_CIRCUMFERENCE * latCosine2; const altPixelsPerMeter2 = altPixelsPerDegree2 / pixelsPerDegreeY * altPixelsPerMeter; result.pixelsPerDegree2 = [0, pixelsPerDegreeY2, altPixelsPerDegree2]; result.pixelsPerMeter2 = [altPixelsPerMeter2, 0, altPixelsPerMeter2]; } // Main results, used for converting meters to latlng deltas and scaling offsets return result; }
对于project_uCommonUnitsPerWorldUnit来说就是计算在精度和纬度上,一度代表的瓦片像素数目。对于project_uCommonUnitsPerWorldUnit2来说这里面用了一个泰勒级数的二阶展开(咨询了下管戈,泰勒级数展开项越多代表模拟值误差越小,这里用到了第二级)主要是在着色器中在`project_uCommonUnitsPerWorldUnit + project_uCommonUnitsPerWorldUnit2 * dy`这里做精度补偿
gl.uniform3f(this.project_uCommonUnitsPerWorldUnit, distanceScles.pixelsPerDegree[0], distanceScles.pixelsPerDegree[1], distanceScles.pixelsPerDegree[2]);
vec2 project_offset(vec2 offset) { float dy = offset.y; // if (project_uCoordinateSystem == COORDINATE_SYSTEM_LNGLAT_AUTO_OFFSET) { dy = clamp(dy, -1., 1.); // } vec3 commonUnitsPerWorldUnit = project_uCommonUnitsPerWorldUnit + project_uCommonUnitsPerWorldUnit2 * dy; // return vec4(offset.xyz * commonUnitsPerWorldUnit, offset.w); return vec2(offset.xy * commonUnitsPerWorldUnit.xy); } // 返回在v3 api中的3d坐标系下的坐标, 采用高精度模式 vec2 project_view_local_position3(vec2 latlngHigh, vec2 latlngLow) { vec2 centerCoordHigh = project_position(center.xy + center.zw, zoom); // Subtract high part of 64 bit value. Convert remainder to float32, preserving precision. float X = latlngHigh.x - center.x; float Y = latlngHigh.y - center.y; return project_offset(vec2(X + latlngLow.x, Y + latlngLow.y)); }
最终效果: