原文地址:https://cesiumjs.org/tutorials/Geometry-and-Appearances/
几何体和外观效果(Geometry and Appearances)
这篇教程会教大家学习Primitive API中支持的几何体和外观效果。这篇教程并不是面向Cesium的普通用户,主要讨论Cesium的高级知识,包括自定义三角网(mesh),形状(shape),体(volume)以及他们的外观。如果你是初学者,建议先学下这篇教程。
Cesium可以使用Entity创建不同的几何体,比如多边形和椭圆等。比如把下面代码拷贝到 Sandcastle 的Hello World 就能创建一个带条纹状材质的矩形:
var viewer = new Cesium.Viewer('cesiumContainer');
viewer.entities.add({
rectangle : {
coordinates : Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
material : new Cesium.StripeMaterialProperty({
evenColor: Cesium.Color.WHITE,
oddColor: Cesium.Color.BLUE,
repeat: 5
})
}
});
这篇教程里,我们深入到图元内部,使用
Geometry
类和 Appearance
类来创建效果。几何体定义了图元的结构,比如三角网、线、点等。外观(appearance)定义了图片的着色效果,包含完整的顶点(vertex)和片段(fragment)着色器(shader)以及着色器状态。
Cesium支持下列几何体:
BoxOutlineGeometry
A box
CorridorOutlineGeometry
以米为单位的折线 和 一个挤压高度
CylinderOutlineGeometry
圆柱, 椎体,半椎体
EllipseOutlineGeometry
椭圆或者垂直挤压的椭圆
RectangleOutlineGeometry
矩形或者垂直挤压矩形
PolygonOutlineGeometry
多边形,支持带洞以及垂直挤压
SimplePolylineGeometry
像素宽度定义的折线段
PolylineVolumeOutlineGeometry
一个二维图形沿着折线的延伸体。
WallOutlineGeometry
垂直于地表的墙面
使用几何体和外观的优势:
性能 - 尤其是绘制大量静态图元(比如整个美国的邮政编码区域多边形),使用几何体可以把他们组合成一个单一的几何体,这样会减少cpu的开销,并且充分利用GPU的能力。组合几何体可以在web worker中完成,不会影响用户界面的响应。
灵活性 - 图元由几何体和外观构成。不过他们可以单独修改。新建的几何体可以兼容多种不同的外观,反之亦然。
-
底层访问 - 外观提供了近乎最底层的渲染访问,但是又不需要直接担心渲染
Renderer
的细节技术 。外观使下面的技术简单了很多:- 编写完整的顶点和片段着色器GLSL代码。
- 使用用户自定义的渲染状态。
当然也有一些缺点:
- 使用几何体和外观需要写更多的代码,并且需要对图形知识有深刻的理解。Entity是应用层的抽象;而几何体和外观更像是一个传统3D引擎的级别。
- 对于静态数据,几何体合并非常有效,但是对于动态数据不适合。
使用几何体和外观来重新编写示例代码:
var viewer = new Cesium.Viewer('cesiumContainer');
var scene = viewer.scene;
// 原始代码
//viewer.entities.add({
// rectangle : {
// coordinates : Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
// material : new Cesium.StripeMaterialProperty({
// evenColor: Cesium.Color.WHITE,
// oddColor: Cesium.Color.BLUE,
// repeat: 5
// })
// }
//});
var instance = new Cesium.GeometryInstance({
geometry : new Cesium.RectangleGeometry({
rectangle : Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
vertexFormat : Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT
})
});
scene.primitives.add(new Cesium.Primitive({
geometryInstances : instance,
appearance : new Cesium.EllipsoidSurfaceAppearance({
material : Cesium.Material.fromType('Stripe')
})
}));
没有用矩形的entity,我们使用了普通的 Primitive
, 它里面连接和几何体和外观。现在先忽略 Geometry
和 a GeometryInstance
的区别,只需知道instance是geometry的容器。
创建矩形几何体 RectangleGeometry
的时候,这个矩形区域的三角网会贴合地球曲率。
因为我们预先知道这个几何体是在球面上,所以直接使用 EllipsoidSurfaceAppearance
。这样做也能节省内存 ,支持所有的材质,因为几何体是在椭球体上方的固定高度(译者注:个人理解是说顶点可以只需要二维坐标,高度值可以当作uniform传进去)。
几何体合并
当使用一个图元去绘制多个静态几何体的时候,会有些效率提升。比如我们画两个矩形:
var viewer = new Cesium.Viewer('cesiumContainer');
var scene = viewer.scene;
var instance = new Cesium.GeometryInstance({
geometry : new Cesium.RectangleGeometry({
rectangle : Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
vertexFormat : Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT
})
});
var anotherInstance = new Cesium.GeometryInstance({
geometry : new Cesium.RectangleGeometry({
rectangle : Cesium.Rectangle.fromDegrees(-85.0, 20.0, -75.0, 30.0),
vertexFormat : Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT
})
});
scene.primitives.add(new Cesium.Primitive({
geometryInstances : [instance, anotherInstance],
appearance : new Cesium.EllipsoidSurfaceAppearance({
material : Cesium.Material.fromType('Stripe')
})
}));
创建了另一个矩形的instance,然后把两个instance都添加到一个图元里,使用同一个外观去绘制。 一些外观允许为每个instance设置不同的属性(attribute)。比如,使用 PerInstanceColorAppearance
对每个instance着不同颜色。
var viewer = new Cesium.Viewer('cesiumContainer');
var scene = viewer.scene;
var instance = new Cesium.GeometryInstance({
geometry : new Cesium.RectangleGeometry({
rectangle : Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
vertexFormat : Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
}),
attributes : {
color : new Cesium.ColorGeometryInstanceAttribute(0.0, 0.0, 1.0, 0.8)
}
});
var anotherInstance = new Cesium.GeometryInstance({
geometry : new Cesium.RectangleGeometry({
rectangle : Cesium.Rectangle.fromDegrees(-85.0, 20.0, -75.0, 30.0),
vertexFormat : Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
}),
attributes : {
color : new Cesium.ColorGeometryInstanceAttribute(1.0, 0.0, 0.0, 0.8)
}
});
scene.primitives.add(new Cesium.Primitive({
geometryInstances : [instance, anotherInstance],
appearance : new Cesium.PerInstanceColorAppearance()
}));
每个intance有一个Color
属性。图元里创建一个PerInstanceColorAppearance
,它知道使用每个instance的color属性去着色。
几何体合并允许Cesium高效的渲染大量几何体。下面示例绘制了2592个不同颜色的矩形。优化之后,渲染非常块。
var viewer = new Cesium.Viewer('cesiumContainer');
var scene = viewer.scene;
var instances = [];
for (var lon = -180.0; lon < 180.0; lon += 5.0) {
for (var lat = -85.0; lat < 85.0; lat += 5.0) {
instances.push(new Cesium.GeometryInstance({
geometry : new Cesium.RectangleGeometry({
rectangle : Cesium.Rectangle.fromDegrees(lon, lat, lon + 5.0, lat + 5.0),
vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
}),
attributes : {
color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.fromRandom({alpha : 0.5}))
}
}));
}
}
scene.primitives.add(new Cesium.Primitive({
geometryInstances : instances,
appearance : new Cesium.PerInstanceColorAppearance()
}));
拾取
当instance合并之后,仍然支持独立访问。通常,我们会设置一个id
属性, Scene.pick
函数里通过它来判定哪个instance被拾取。这个id
可以任何js类型:字符串,数字,带属性的对象等等。
下面的示例创建一个带id
的instance,当它被点击的时候控制台会输出一个消息。
var viewer = new Cesium.Viewer('cesiumContainer');
var scene = viewer.scene;
var instance = new Cesium.GeometryInstance({
geometry : new Cesium.RectangleGeometry({
rectangle : Cesium.Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0),
vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
}),
id : 'my rectangle',
attributes : {
color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.RED)
}
});
scene.primitives.add(new Cesium.Primitive({
geometryInstances : instance,
appearance : new Cesium.PerInstanceColorAppearance()
}));
var handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
handler.setInputAction(function (movement) {
var pick = scene.pick(movement.position);
if (Cesium.defined(pick) && (pick.id === 'my rectangle')) {
console.log('Mouse clicked rectangle.');
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
使用id
而不是用instance对象本身去判定,主要是为了避免在创建图元之后,我们的图元甚至我们的项目对所有的instance对象 以及 它的几何体 一直被引用无法释放内存。因为几何体一般包含了一个比较大的数组,这种方式就可以帮我们节省大量内存。
几何体intances
目前为止,我们创建的每个几何体instance都只包含一个几何体。此外,instance竟然用来把同一个几何体放置在场景的不同位置,包括不同大小和方向。由于多个instance可以引用同一个几何体( Geometry
),而每个instance可以有不同的偏移矩阵(modelMatrix)。这样,我们就只需要计算一次几何体(计算顶点等)而多次使用它。
下面的代码创建了一个EllipsoidGeometry
和 两个instance. 每个instance 引用了相同的椭球几何体,但是使用 modelMatrix
放到不同位置,这里效果是一个叠在另一个之上。
var viewer = new Cesium.Viewer('cesiumContainer');
var scene = viewer.scene;
var ellipsoidGeometry = new Cesium.EllipsoidGeometry({
vertexFormat : Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
radii : new Cesium.Cartesian3(300000.0, 200000.0, 150000.0)
});
var cyanEllipsoidInstance = new Cesium.GeometryInstance({
geometry : ellipsoidGeometry,
modelMatrix : Cesium.Matrix4.multiplyByTranslation(
Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(-100.0, 40.0)),
new Cesium.Cartesian3(0.0, 0.0, 150000.0),
new Cesium.Matrix4()
),
attributes : {
color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.CYAN)
}
});
var orangeEllipsoidInstance = new Cesium.GeometryInstance({
geometry : ellipsoidGeometry,
modelMatrix : Cesium.Matrix4.multiplyByTranslation(
Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(-100.0, 40.0)),
new Cesium.Cartesian3(0.0, 0.0, 450000.0),
new Cesium.Matrix4()
),
attributes : {
color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.ORANGE)
}
});
scene.primitives.add(new Cesium.Primitive({
geometryInstances : [cyanEllipsoidInstance, orangeEllipsoidInstance],
appearance : new Cesium.PerInstanceColorAppearance({
translucent : false,
closed : true
})
}));
更新每个instance的属性
即便是已经添加到图元里,每个instance的一些属性也可以修改,包括:
- Color :
ColorGeometryInstanceAttribute
决定了几何体颜色。不过图元应该设置一个PerInstanceColorAppearance
外观。 - Show :布尔变量决定instance是否可见,对任意instance都有效。
下面代码演示如何修改几何体instance的颜色:
This example shows how to change the color of the geometry instance:
var viewer = new Cesium.Viewer('cesiumContainer');
var scene = viewer.scene;
var circleInstance = new Cesium.GeometryInstance({
geometry : new Cesium.CircleGeometry({
center : Cesium.Cartesian3.fromDegrees(-95.0, 43.0),
radius : 250000.0,
vertexFormat : Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
}),
attributes : {
color : Cesium.ColorGeometryInstanceAttribute.fromColor(new Cesium.Color(1.0, 0.0, 0.0, 0.5))
},
id: 'circle'
});
var primitive = new Cesium.Primitive({
geometryInstances : circleInstance,
appearance : new Cesium.PerInstanceColorAppearance({
translucent : false,
closed : true
})
});
scene.primitives.add(primitive);
setInterval(function() {
var attributes = primitive.getGeometryInstanceAttributes('circle');
attributes.color = Cesium.ColorGeometryInstanceAttribute.toValue(Cesium.Color.fromRandom({alpha : 1.0}));
},2000);
几何体的属性需要通过 primitive.getGeometryInstanceAttributes
来获取到。attributes
里的值可以直接修改。这里,我们每2秒钟设置'circle'这个几何体随机颜色。
外观(Appearances)
几何体定义了结构。图元的另一个关键属性是appearance
,决定图元的着色,也就说每个像素是如何上色的。一个图元可以有若干个几何体instance,但是只能有一个appearance属性。根据appearance类型不同,一个appearance可能有一个 material
属性,材质属性决定了大体的着色( the bulk of the shading)。
Cesium 包含下述外观类型:
MaterialAppearance
所有几何体都使用同一个外观,支持使用 materials 去定义着色效果.
EllipsoidSurface
MaterialAppearance
的简化版本,假定几何体都和地球椭球体平行,就像多边形一样。使用这个可以在计算大量顶点属性的时候节省内存
PerInstanceColorAppearance
每个instance使用不同的颜色去着色。
PolylineMaterialAppearance
支持在折线上设置材质。
PolylineColorAppearance
支持折线在每个顶点或者每一段设置颜色。
外观完整的定义了顶点和片段着色器代码,在GPU中图元渲染的时候使用。除非要自定义外观,否则我们很少使用它们。外观也定义了完整的渲染你状态,它控制了图元渲染时候的GPU状态。我们可以使用高级的属性来定义渲染状态,比如 闭合closed
和 半透明translucent
,外观会把他们转换为真正的底层状态,比如:
// 一个不透明的盒子,视点永远不会进到里面去
// 那么就需要启用背面裁剪,深度检测,不需要混合。
var appearance = new Cesium.PerInstanceColorAppearance({
translucent : false,
closed : true
});
// 这个和上面的设置等价
var anotherAppearance = new Cesium.PerInstanceColorAppearance({
renderState : {
depthTest : {
enabled : true
},
cull : {
enabled : true,
face : Cesium.CullFace.BACK
}
}
});
一旦我们的外观创建了,我们不能修改它的renderState
属性,但是我们能修改它的材质 material
。当然,我们可以整个替换图元的appearance
属性。
大部分外观包含 flat
和faceForward
属性, 这个直接控制了GLSL的着色效果。
-
flat
- 纯色着色,不考虑光照效果。 -
faceForward
- 当有光照的的时候,当视图正对它的时候反转法向量,避免墙体的背面是黑色的。
flat : true | faceForward : false | faceForward : true |
几何体和外观的匹配性
我们发现不是所有的外观都能作用在任意几何体上。比如EllipsoidSurfaceAppearance
不能用在WallGeometry
上,因为墙永远垂直地表,而不是平行地表。
隐含之意,一个外观能和一个几何体匹配,需要顶点格式匹配,也就是说几何体必须包含外观需要的顶点格式数据。创建一个几何体的时候,可以指定一个 VertexFormat
参数。
有时候为了简化问题,但是接受一点点浪费和效率低,可以计算一个几何体的所有顶点属性格式,这样就能和所有外观兼容(忽略per-instance属性)
var geometry = new Cesium.RectangleGeometry({
vertexFormat : Cesium.VertexFormat.ALL
// ...
});
如果使用EllipsoidSurfaceAppearance
,比如我们只创建了顶点的位置属性,那么就会崩溃(get away)。
var geometry = new Ceisum.RectangleGeometry({
vertexFormat : Ceisum.VertexFormat.POSITION_ONLY
// ...
});
通常,我们怎么知道某种外观需要哪种顶点格式?大部分外观都有一个 vertexFormat
属性, 甚至一个 VERTEX_FORMAT
静态常量。
var geometry = new Ceisum.RectangleGeometry({
vertexFormat : Ceisum.EllipsoidSurfaceAppearance.VERTEX_FORMAT
// ...
});
var geometry2 = new Ceisum.RectangleGeometry({
vertexFormat : Ceisum.PerInstanceColorAppearance.VERTEX_FORMAT
// ...
});
var appearance = new Ceisum.MaterialAppearance(/* ... */);
var geometry3 = new Ceisum.RectangleGeometry({
vertexFormat : appearance.vertexFormat
// ...
});
同样,几何体的 vertexFormat
属性也决定了几何体是否可以合并。如果要合并,可以几何体类型不同,但是必须保证顶点格式一致。
相关资源
用户手册:
想了解材质的更多内容,请访问Fabric。
想了解这块的开发计划,请访问: Geometry and Appearances Roadmap.