化石原创文章,转载请注明来源并保留原文链接


项目中,倾斜摄影模型从osgb而来,使用3dtile工具转化成了3d tiles格式供Cesium使用。然后服务器会给出一定的坐标。倾斜摄影模型的坐标和服务器给出的坐标都是基于wgs84的。

如果使用google地图,只使用卫星地图( http://mt1.google.cn/vt/lyrs=s&hl=zh-CN&x={x}&y={y}&z={z}&s=Gali )的话,那么倾斜摄影和服务器给的坐标,都能和卫星地图完美配合。无需做任何事情。

然而地图上使用的时候必须看到道路标识,所以给到Cesium的tiles路径就变成了: http://mt1.google.cn/vt/lyrs=y&hl=zh-CN&x={x}&y={y}&z={z}&gl=cn。

可以看出来,请求的api中,参数lyrs从s变成了y,这个决定了从google服务器上请求下来的瓦片图,带有道路合成。参数也多了一个gl=cn,没有这个参数的话,会发现请求到的瓦片图,道路和卫星图是不匹配的,也就是道路有偏差。

所以瓦片请求路径就成了带gl=cn的那个。

不过使用这个瓦片图,其实google是为了符合中国规范,对图做了偏移。所以,所有wgs84的坐标信息(原来的),在这份地图上看起来都有了错位。因而我们需要解决两个问题:

1、tileset在该地图上的位置改动

2、服务器给到wgs84坐标后,我们得换算成现在地图上的坐标

下面是相关的方式:

1、tileset

所涉项目tileset数量很少,所以采用手工方式改动经纬度,看位置信息。最后记录下合适的经纬度。最后配合下面的代码,使tileset在加载准备好以后,自动放置到最后的位置。

var params = {
    tx: 120.7521311337607,    //longitude
    ty: 31.19968924358735,    //latitude
    tz: 0.1,                  //height
    rx: 0,                    //degree
    ry: 0,                    //degree
    rz: 0                     //degree
};
	
function update3dtilesMaxtrix(tileset, params) {
    // rotation maxtrix construct, first we get matrix3, then matrix4 from matrix3
    var mx = Cesium.Matrix3.fromRotationX(Cesium.Math.toRadians(params.rx));
    var my = Cesium.Matrix3.fromRotationY(Cesium.Math.toRadians(params.ry));
    var mz = Cesium.Matrix3.fromRotationZ(Cesium.Math.toRadians(params.rz));
    var rotationX = Cesium.Matrix4.fromRotationTranslation(mx);
    var rotationY = Cesium.Matrix4.fromRotationTranslation(my);
    var rotationZ = Cesium.Matrix4.fromRotationTranslation(mz);

    // translation matrix4
    var position = Cesium.Cartesian3.fromDegrees(params.tx, params.ty, params.tz);
    var m = Cesium.Transforms.eastNorthUpToFixedFrame(position);

    // concate translation and all rotations
    Cesium.Matrix4.multiply(m, rotationX, m);
    Cesium.Matrix4.multiply(m, rotationY, m);
    Cesium.Matrix4.multiply(m, rotationZ, m);

    // update tileset's matrix4
    tileset._root.transform = m;
}

viewer.scene.primitives.add(tileset).readyPromise.then(function(t) {
	update3dtilesMaxtrix(t, params);
});

viewer.zoomTo(tileset);

2、服务器给的wgs84坐标信息

使用gcoord库,把每个wgs84坐标转化成gcj02坐标即可。示例代码:

var result = gcoord.transform(
  [116.403988, 39.914266],    // 经纬度坐标
  gcoord.WGS84,               // 当前坐标系
  gcoord.GCJ02                // 目标坐标系
);

化石原创文章,转载请注明来源并保留原文链接



化石原创文章,转载请注明来源并保留原文链接


拿到手的这个倾斜摄影数据是一个文件夹,里面放置osgb.s3c,metadata.xml,和一个Data目录。Data目录有一堆Tile文件夹,里面放置了很多的osgb后缀的文件-Smart3D处理过的Open Scene Graph Binary文件。

注:

metadata.xml中的信息有SRS和SRSOrigin,分别对应模型建立的坐标系,和原点位置。如下,SRSOrigin可以在epsg.io网站上查EPSG:4544下的地点,就是该模型的中心位置。

<SRS>EPSG:4544</SRS>
<!--Origin in Spatial Reference System-->
<SRSOrigin>701356,3307142,0</SRSOrigin>

查了一下网络,需要用一些工具转化该数据集才能在Cesium中显示,因为Cesium目前只支持3D tiles。

1、 https://github.com/fanvanzh/3dtiles 这里下载预编译好的一个工具

windows版预编译好的下载:

链接:https://pan.baidu.com/s/1jZcSVIUoYNtMQdxbo4xJCQ
提取码:qjsj

2、命令行打开,使用下面的命令把拿到手的文件夹里的数据转化成3d tiles,会生成在./output下

3dtile -f osgb -i ./ -o ./output

3、拷贝./output里的3d tiles文件到Cesium的工程目录,用下面的代码显示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
    <meta name="description" content="Demo the minimum set of options needed to remove all external dependencies, for offline operation.">
    <meta name="cesium-sandcastle-labels" content="Beginner, Showcases">
    <title>Cesium Demo</title>
    <script type="text/javascript" src="../Sandcastle-header.js"></script>
    <script type="text/javascript" src="../../../ThirdParty/requirejs-2.1.20/require.js"></script>
    <script type="text/javascript">
    require.config({
        baseUrl : '../../../Source',
        waitSeconds : 60
    });
    </script>
</head>
<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
<style>
    @import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>
<script id="cesium_sandcastle_script">
    function startup(Cesium) {
        'use strict';
  
        var fixGltf = function(gltf) {
            if (!gltf.extensionsUsed) {
                return;
            }

            var v = gltf.extensionsUsed.indexOf('KHR_technique_webgl');
            var t = gltf.extensionsRequired.indexOf('KHR_technique_webgl');
            // 中招了。。
            if (v !== -1) {
                gltf.extensionsRequired.splice(t, 1, 'KHR_techniques_webgl');
                gltf.extensionsUsed.splice(v, 1, 'KHR_techniques_webgl');
                gltf.extensions = gltf.extensions || {};
                gltf.extensions['KHR_techniques_webgl'] = {};
                gltf.extensions['KHR_techniques_webgl'].programs = gltf.programs;
                gltf.extensions['KHR_techniques_webgl'].shaders = gltf.shaders;
                gltf.extensions['KHR_techniques_webgl'].techniques = gltf.techniques;
                var techniques = gltf.extensions['KHR_techniques_webgl'].techniques;

                gltf.materials.forEach(function (mat, index) {
                    gltf.materials[index].extensions['KHR_technique_webgl'].values = gltf.materials[index].values;
                    gltf.materials[index].extensions['KHR_techniques_webgl'] = gltf.materials[index].extensions['KHR_technique_webgl'];

                    var vtxfMaterialExtension = gltf.materials[index].extensions['KHR_techniques_webgl'];

                    for (var value in vtxfMaterialExtension.values) {
                        var us = techniques[vtxfMaterialExtension.technique].uniforms;
                        for (var key in us) {
                            if (us[key] === value) {
                                vtxfMaterialExtension.values[key] = vtxfMaterialExtension.values[value];
                                delete vtxfMaterialExtension.values[value];
                                break;
                            }
                        }
                    };
                });

                techniques.forEach(function (t) {
                    for (var attribute in t.attributes) {
                        var name = t.attributes[attribute];
                        t.attributes[attribute] = t.parameters[name];
                    };

                    for (var uniform in t.uniforms) {
                        var name = t.uniforms[uniform];
                        t.uniforms[uniform] = t.parameters[name];
                    };
                });
            }
        }

        Object.defineProperties(Cesium.Model.prototype, {
            _cachedGltf: {
                set: function (value) {
                    this._vtxf_cachedGltf = value;
                    if (this._vtxf_cachedGltf &amp;&amp; this._vtxf_cachedGltf._gltf) {
                        fixGltf(this._vtxf_cachedGltf._gltf);
                    }
                },
                get: function () {
                    return this._vtxf_cachedGltf;
                }
            }
        });



        //Sandcastle_Begin
        // This is an example of using Cesium "Offline", meaning disconnected from the
        // external Internet.  It must still be served from a local web server, but
        // does not rely on any outside resources or services.  For more info, see:
        // https://github.com/AnalyticalGraphicsInc/cesium/wiki/Offline-Guide
        var viewer = new Cesium.Viewer('cesiumContainer', {
            imageryProvider : Cesium.createTileMapServiceImageryProvider({
                url : Cesium.buildModuleUrl('Assets/Textures/NaturalEarthII')
            }),
            baseLayerPicker : false,
            geocoder : false
        });

        var tileset = new Cesium.Cesium3DTileset({
            url: '../../SampleData/Cesium3DTiles/fromyuzs/tileset.json'
        });

        viewer.scene.primitives.add(tileset);
        viewer.zoomTo(tileset);

        //viewer.scene.debugShowGlobeDepth = true;
        //Sandcastle_End
        Sandcastle.finishedLoading();
    }
    if (typeof Cesium !== 'undefined') {
        startup(Cesium);
    } else if (typeof require === 'function') {
        require(['Cesium'], startup);
    }
</script>
</body>
</html>

注意:代码使用了网络上找到的fixGltf方式,否则Cesium显示这个3d tiles的时候会出错。


化石原创文章,转载请注明来源并保留原文链接



化石原创文章,转载请注明来源并保留原文链接


JavaScript中,我所知的通过http下载一般是XMLHttpRequest()。所以突然想看一下瓦片下载流程的时候,就着这个目的地看了一下,还是比较顺利的。因为看的比较快,没有记录太多东西。但是从开始到中间经历的流程(程序调用顺序)还是拿下来了。

Globe.render()会侦测tile是否需要下载。如要下载,会放在QuadtreePrimitive的某个queue(有三个queue)里面。这个tile信息记录了一个tile的足够信息,比如z,x、y,url。

Globe.endFrame()里有QuadtreePrimitive.endFrame(), 有processTileLoadQueue(),会处理到这个queue里面的每个tile。

最后实际处理会是GlobeSurfaceTileProvider.loadTile()发起,经历的路程如下:

GlobeSurfaceTile.doRequest()
   CesiumTerrainProvider.requestTileGeometry
     Resource.fetchArrayBuffer
       Resource.fetch
         Resource._makeRequest
           RequestScheduler.request
             startRequest
               Resource.loadWidthXhr
                 new XMLHttpRequest()

化石原创文章,转载请注明来源并保留原文链接



化石原创文章,转载请注明来源并保留原文链接


这篇文章,我们能知道写Cesium shader,可以使用不少引擎已经定义好的变量和方法,而不必要造轮子。如果有心的话,你会发现Cesium并没有像其他大多3D引擎一样,需要你使用一些类似#include的手段去包含相关的文件。那么问题就来了,这些变量或者方法,在OpenGL ES驱动编译的时候,总是要提前申明和实现的。就一定程度上来说,我们写的,在眼睛里看到的代码并不完整。Cesium会在一定时候为我们补足缺失的部分。

还是以SkyBoxFS.glsl举个例子,这个glsl代码如下(原始的):

uniform samplerCube u_cubeMap;

varying vec3 v_texCoord;

void main()
{
    vec4 color = textureCube(u_cubeMap, normalize(v_texCoord));
    gl_FragColor = vec4(czm_gammaCorrect(color).rgb, czm_morphTime);
}

经过Cesium在合适的时机处理后(其实在ShaderCache.prototype.getShaderProgram()方法中),最后完整的glsl代码是这样:

#ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
#else
    precision mediump float;
#endif

#define HDR
#define OES_texture_float_linear

uniform float czm_gamma;
uniform float czm_morphTime;

vec3 czm_gammaCorrect(vec3 color) {
#ifdef HDR
    color = pow(color, vec3(czm_gamma));
#endif
    return color;
}

vec4 czm_gammaCorrect(vec4 color) {
#ifdef HDR
    color.rgb = pow(color.rgb, vec3(czm_gamma));
#endif
    return color;
}
#line 0
#line 0
uniform samplerCube u_cubeMap;

varying vec3 v_texCoord;

void main()
{
    vec4 color = textureCube(u_cubeMap, normalize(v_texCoord));
    gl_FragColor = vec4(czm_gammaCorrect(color).rgb, czm_morphTime);
}

两相对比,显然Cesium自动添加了关联的czm变量和方法到glsl代码中,使其成为了一个合格的glsl代码。我看了这段代码后,发现了Cesium在这个过程构造依赖树的方式,觉得很有用,遂记录下来。

涉及的方法是ShaderSource.js里的,getBuiltinsAndAutomaticUniforms()。

function getBuiltinsAndAutomaticUniforms(shaderSource) {
    // generate a dependency graph for builtin functions
    var dependencyNodes = [];
    var root = getDependencyNode('main', shaderSource, dependencyNodes);
    generateDependencies(root, dependencyNodes);
    sortDependencies(dependencyNodes);

    // Concatenate the source code for the function dependencies.
    // Iterate in reverse so that dependent items are declared before they are used.
    var builtinsSource = '';
    for (var i = dependencyNodes.length - 1; i >= 0; --i) {
        builtinsSource = builtinsSource + dependencyNodes[i].glslSource + '\n';
    }

    return builtinsSource.replace(root.glslSource, '');
}

重要的三个方法:getDependencyNode(), generateDependencies(), sortDependencies()。我们一一来读。

function getDependencyNode(name, glslSource, nodes) {
        var dependencyNode;

        // check if already loaded
        for (var i = 0; i < nodes.length; ++i) {
            if (nodes[i].name === name) {
                dependencyNode = nodes[i];
            }
        }

        if (!defined(dependencyNode)) {
            // strip doc comments so we don't accidentally try to determine a dependency for something found
            // in a comment
            glslSource = removeComments(glslSource);

            // create new node
            dependencyNode = {
                name : name,
                glslSource : glslSource,
                dependsOn : [],
                requiredBy : [],
                evaluated : false
            };
            nodes.push(dependencyNode);
        }

        return dependencyNode;
    }

第2行,定义了一个最后用来返回的节点。

第5到9行,轮询参数3代表的已经存在的节点集合,看参数1名字代表的节点是否已经在该节点集合中存在。如果存在,那么我们返回的就是这个节点。

第11到25行,如果上面没有查到节点,那么我们会创建一个节点出来,这个节点有name(名字,作为后续查询的key)、glslsource(原始的glsl代码)、dependsOn(依赖的节点集合)、requireBy(被依赖的节点集合)、evaluated(是否评估过的标志)。并且这个节点会放置到参数3代表的集合里。

所以,这个方法正如它的方法名,就是在参数3代表的集合中查询名字为参数1的节点是否存在,不存在就创建一个新的存入集合。返回的是已经存在的节点或者是新创建的节点。

以我们上面的 getBuiltinsAndAutomaticUniforms ()方法来说,第一次会创建出一个名字为“main”的节点出来。假设我们的glsl代码是这篇文章最开始的那段的话,那么这个节点的glslsource就是整个代码内容。dependsOn数组是空,表示不依赖任何节点。requireBy数组为空,表示也没有被其他任何节点依赖。evaluated是false,还未经评估。

然后就到generateDependencies:

    function generateDependencies(currentNode, dependencyNodes) {
        if (currentNode.evaluated) {
            return;
        }

        currentNode.evaluated = true;

        // identify all dependencies that are referenced from this glsl source code
        var czmMatches = currentNode.glslSource.match(/\bczm_[a-zA-Z0-9_]*/g);
        if (defined(czmMatches) &amp;&amp; czmMatches !== null) {
            // remove duplicates
            czmMatches = czmMatches.filter(function(elem, pos) {
                return czmMatches.indexOf(elem) === pos;
            });

            czmMatches.forEach(function(element) {
                if (element !== currentNode.name &amp;&amp; ShaderSource._czmBuiltinsAndUniforms.hasOwnProperty(element)) {
                    var referencedNode = getDependencyNode(element, ShaderSource._czmBuiltinsAndUniforms[element], dependencyNodes);
                    currentNode.dependsOn.push(referencedNode);
                    referencedNode.requiredBy.push(currentNode);

                    // recursive call to find any dependencies of the new node
                    generateDependencies(referencedNode, dependencyNodes);
                }
            });
        }
    }

第2行先检测节点的evaluated值,是false的才处理。true说明已经处理过了。

第9行在glslsource里面查找czm_开头的所有字符串,得到一个字符串数组。

第12行到14行,使用Array的filter方法,只取原来数组中的第一个出现的元素。就过滤掉了原来字符串中重复的字符串。

第16到25行,对每个字符串,以这个字符串为名字做节点,生成这个节点的依赖集合(第19行),和填充它的被依赖集合(第20行)。然后递归计算该节点的依赖(第23行)。

还是以上面我们的第一个glsl的main为例,上面一段我们可以看到,”main”节点生成,依赖集合空、被依赖集合空,evaluated是false。这个节点作为参数1送入方法,第一时间evaluated会被设置成true。第9行运行过后,czmMatches集合会放入czm_gammaCorrect、czm_morphTime字符串。

我们不管其他,到19行跑过,我们的main节点能想象到依赖集合里面添置czm_gammaCorrect依赖,下一个循环,这一行又会添置czm_morphTime依赖。

而20行的,referenceNode,以czm_gammaCorrect为例,它的requireBy就会加入我们的main节点。

所以,一个main依赖czm_gammaCorrect,czm_gammaCorrect被main依赖的代码表示就完成了。可以想象,如果是一段复杂的glsl代码,里面有很多的czm_xxx,而czm_xxx可能还依赖其他的czm_xxx,那么这个就可以形成一颗完整的依赖树。根节点如我们例子中的main,以它为根,它依赖的每个节点都是一棵树的枝干或者叶子。枝繁叶茂。

最后是sortDependencies方法:

function sortDependencies(dependencyNodes) {
	//记录不被依赖任何节点的节点(比如我们的main)
	var nodesWithoutIncomingEdges = [];
	//记录所有的节点
	var allNodes = [];

	while (dependencyNodes.length > 0) {
		//下面两行代码,从dependencyNodes中取出节点,放入allNodes
		//整个while循环跑下来,dependencyNodes就空了,节点全部转移到allNodes
		var node = dependencyNodes.pop();
		allNodes.push(node);

		if (node.requiredBy.length === 0) {
			nodesWithoutIncomingEdges.push(node);
		}
	}

	while (nodesWithoutIncomingEdges.length > 0) {
		var currentNode = nodesWithoutIncomingEdges.shift();

		dependencyNodes.push(currentNode);

		for (var i = 0; i < currentNode.dependsOn.length; ++i) {
			// remove the edge from the graph
			//检测节点依赖的节点,让后者的requireBy去除检测的节点(用于检测相互依赖错误)
			var referencedNode = currentNode.dependsOn[i];
			var index = referencedNode.requiredBy.indexOf(currentNode);
			referencedNode.requiredBy.splice(index, 1);

			// if referenced node has no more incoming edges, add to list
			//下面这一行的逻辑处理会这里导致的结果是:越是被依赖的多,在数组中的位置就越靠后。
			//所有在后续的处理上,需要从数组后面倒着拿里面的节点的源代码数据组合成最后的shader源代码
			if (referencedNode.requiredBy.length === 0) {
				nodesWithoutIncomingEdges.push(referencedNode);
			}
		}
	}

	// if there are any nodes left with incoming edges, then there was a circular dependency somewhere in the graph
	var badNodes = [];
	for (var j = 0; j < allNodes.length; ++j) {
		if (allNodes[j].requiredBy.length !== 0) {
			badNodes.push(allNodes[j]);
		}
	}

	//>>includeStart('debug', pragmas.debug);
	if (badNodes.length !== 0) {
		var message = 'A circular dependency was found in the following built-in functions/structs/constants: \n';
		for (var k = 0; k < badNodes.length; ++k) {
			message = message + badNodes[k].name + '\n';
		}
		throw new DeveloperError(message);
	}
	//>>includeEnd('debug');
	
	//没有返回值,我们的参数就是处理过的数据,所有的节点的requireBy.length是0。
}

这里我换了个方式,一些代码细节说明放在了代码中,也就是上面的中文注释。唯一要说明的是,在节点的requireBy集合被处理成空后, 代码中的 nodesWithoutIncomingEdges 数组用来安置这个节点。整个方法是

1、侦测循环依赖(排错)和

2、给节点排序用的:一个节点被依赖的地方越多,它被放进 nodesWithoutIncomingEdges集合的位置就越晚。而 nodesWithoutIncomingEdges里面的值,在18行开始的循环中,会一个个在合适的时机都放入到参数1里面。因此参数1既是入参,又是出参。

三个重要的方法就说明到这里。

最后要说的是,生成依赖树、排序,最终目的都是为完整的glsl代码生成。也就是怎么从我们这篇文章的第一段glsl代码,得到第二段glsl代码。所以,getBuiltinsAndAutomaticUniforms方法中的

var builtinsSource = '';
for (var i = dependencyNodes.length - 1; i >= 0; --i) {
builtinsSource = builtinsSource + dependencyNodes[i].glslSource + '\n';
} 

实际上在做的是最后的拼装工作。builtinsSource开始是空的,每个循环都从数组中取节点的glslsource,加入到builtinsSource中。注意到这个循环是从数组的最后一个元素开始,倒着轮询没?因为:“一个节点被依赖的地方越多,它被放进 nodesWithoutIncomingEdges集合的位置就越晚”。glsl代码本身类似c语言,被依赖的东西一定要先声明,所以才有了排序这个过程。


化石原创文章,转载请注明来源并保留原文链接



化石原创文章,转载请注明来源并保留原文链接


前面有写过一篇关于着色器的,主要是着眼于着色器的流程。在文章中我们知道,着色器代码可以使用GLSL编写(OpenGL ES shader规范, WebGL是从OpenGL ES 2.0规范而来),引擎自带的在 Source\Shaders 下面。如果抓一个代码看一下,比如简单的,SkyBoxFS.glsl,代码如下:

uniform samplerCube u_cubeMap;

varying vec3 v_texCoord;

void main()
{
    vec4 color = textureCube(u_cubeMap, normalize(v_texCoord));
    gl_FragColor = vec4(czm_gammaCorrect(color).rgb, czm_morphTime);
}

我们会看到不少以czm开头的字眼,这种字眼的变量或者方法,是Cesium预先做好的。都在ShaderSource._czmBuiltinsAndUniforms这个字典中。从代码处理来看,这字典里面的值,可以分两类:

1、Builtins

Cesium为方便Shader编写,预先定义好的静态值结构方法。这些都先用glsl定义,然后通过npm run build命令生成相应的js代码。

czm方法,在Shaders/Functions目录下,由CzmBuiltins.js模块引入管理,每一份代码的text都能通过CzmBuilts.js里对应的关键字得到。比如可以通过
“czm_gammaCorrect”这个关键字,得到gammaCorrect.js里面的代码。

czm静态值,在Shaders/Constants目录下,也由CzmBuiltins.js模块管理。

czm常用结构(struct),在Shaders/Structs目录下,一样由CzmBuiltins.js模块管理。

2、AutomacticUniform

Cesium为方便Shader编写,在Renderer/AutomaticUniforms.js里面预先写好的“自动化uniform”。举例:

czm_viewport : new AutomaticUniform({
size : 1,
datatype : WebGLConstants.FLOAT_VEC4,
getValue : function(uniformState) {
return uniformState.viewportCartesian4;
}
}),

就是一个AutomaticUniform。这个东西有一个方法,用来动态生成在shader源代码中插入的声明,代码如下:

AutomaticUniform.prototype.getDeclaration = function(name) {
	var declaration = 'uniform ' + datatypeToGlsl[this._datatype] + ' ' + name;

	var size = this._size;
	if (size === 1) {
		declaration += ';';
	} else {
		declaration += '[' + size.toString() + '];';
	}

	return declaration;
};

也就是这个getDeclaration()会返回类似 “uniform ivec4 czm_viewport;”类似的字眼,恰好是GLSL的声明方式。

上面两类,都会在我们上面提到的ShaderSource._czmBuiltinsAndUniforms字典中存放。这两者的区别在于,前者是“方法、静态值、结构”,在代码角度,这三者是拷贝到shader代码中就不变的-不需要CPU给GPU喂什么外部的值。而后者是GLSL的uniform,对应的值需要在shader代码跑动前,由应用程序给GPU喂送,所以是可变的。这是Cesium区分的标准。


化石原创文章,转载请注明来源并保留原文链接