Cesium官方教程8-- 几何体和外观效果

原文地址: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支持下列几何体:


Box Geometry

BoxGeometry

Box Outline Geometry

BoxOutlineGeometryA box

Circle Geometry

CircleGeometry

Circle Outline Geometry

CircleOutlineGeometry

Corridor Geometry

CorridorGeometry

Corridor Outline Geometry

CorridorOutlineGeometry 以米为单位的折线 和 一个挤压高度

Cylinder Geometry

CylinderGeometry

Cylinder Outline Geometry

CylinderOutlineGeometry圆柱, 椎体,半椎体

Ellipse Geometry

EllipseGeometry

Ellipse Outline Geometry

EllipseOutlineGeometry椭圆或者垂直挤压的椭圆

Ellipsoid Geometry

EllipsoidGeometry

Ellipsoid Outline Geometry

EllipsoidOutlineGeometry椭球体

Extent Geometry

RectangleGeometry

Extent Outline Geometry

RectangleOutlineGeometry矩形或者垂直挤压矩形

Polygon Geometry

PolygonGeometry

Polygon Outline Geometry

PolygonOutlineGeometry多边形,支持带洞以及垂直挤压

Polyline Geometry

PolylineGeometry

Polyline Outline Geometry

SimplePolylineGeometry像素宽度定义的折线段

Volume Geometry

PolylineVolumeGeometry

Volume Outline Geometry

PolylineVolumeOutlineGeometry一个二维图形沿着折线的延伸体。

Sphere Geometry

SphereGeometry

Sphere outline Geometry

SphereOutlineGeometry球体

Wall Geometry

WallGeometry

Wall Outline Geometry

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)。这样,我们就只需要计算一次几何体(计算顶点等)而多次使用它。

几何体 instance

下面的代码创建了一个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
    })
}));

椭球体instances

更新每个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)。

Appearances

Cesium 包含下述外观类型:

MaterialAppearance

MaterialAppearance 所有几何体都使用同一个外观,支持使用 materials 去定义着色效果.

EllipsoidSurface

EllipsoidSurface MaterialAppearance 的简化版本,假定几何体都和地球椭球体平行,就像多边形一样。使用这个可以在计算大量顶点属性的时候节省内存

PerInstanceColorAppearance

PerInstanceColorAppearance 每个instance使用不同的颜色去着色。

PolylineMaterialAppearance

PolylineMaterialAppearance 支持在折线上设置材质。

PolylineColorAppearance

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属性。

大部分外观包含 flatfaceForward 属性, 这个直接控制了GLSL的着色效果。

  • flat - 纯色着色,不考虑光照效果。
  • faceForward - 当有光照的的时候,当视图正对它的时候反转法向量,避免墙体的背面是黑色的。
    flat : true | faceForward : false | faceForward : true |
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.

中国最专业的Cesium开发者社区

推荐阅读更多精彩内容