Mars3D开发基础:Property属性机制

1,047 阅读10分钟

Mars3D开发基础系列文档导航

  1. 三维场景 Map
  2. 地图控件
  3. 地图图层 Layer
  4. 加载DEM地形
  5. 栅格瓦片图层
  6. 坐标系及坐标变换
  7. 相机Camera及视角控制
  8. 时钟Clock及时序控制
  9. 事件机制
  10. 矢量图层Layer
  11. 矢量数据Graphic
  12. Property属性机制
  13. Material材质
  14. glTF小模型
  15. 3DTiles三维模型
  16. 场景特效
  17. 管理及分析功能

平台是数据驱动和 time-dynamic visualization,这些可都是仰仗Property属性机制来实现的。

Property最大的特点是和时间相互关联,在不同的时间可以动态地返回不同的属性值。而Entity则可以感知这些Property的变化,在不同的时间驱动物体进行动态展示。

下面讲解对应的示例:mars3d.cn/editor.html…

运行效果

image.png

1.为什么要用Property?

还是举个例子来说吧。比如我想在地球上的某个位置加一个盒子,可以这样写代码:

// 创建盒子
var marsBox = new mars3d.graphic.BoxEntity({
  position: [117.220164, 31.834887, 39.6],
  style: {
    dimensions: new Cesium.Cartesian3(400.0, 300.0, 500.0),
    color: 'rgba(0,255,255,0.8)',
    outline: true,
  },
})
graphicLayer.addGraphic(marsBox)

image.png

box加在固定位置。 但是呢,如果我想让这个盒子逐渐变长,该怎么操作呢?如下图所示:

image.png

盒子逐渐变长 方法是有的,就是可以不停地去修改marsBox.position,类似这样:

setInterval(function(){ marsBox.entityGraphic.dimensions = xxx; }, 200);

如果场景中有很多物体,在不同的时间段要发生各种走走停停地运动时,这样操作可能会很累人。那么Cesium就提供一种机制,让dimensions可以随时间自动发生变化,自动赋予不同的数值(位置)。这也就是property的作用了。以下代码的加入,就可以让盒子如上图所示做线性运动了。

var property = new Cesium.SampledProperty(Cesium.Cartesian3)
property.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-25 08:00:00')), new Cesium.Cartesian3(400.0, 300.0, 100.0))
property.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-25 08:00:20')), new Cesium.Cartesian3(400.0, 300.0, 900.0))
marsBox.setStyle({ dimensions: property })

以上代码的意思就是在两个不同的时间点分别赋予不同的位置,用SampledProperty包装成一个property,最后赋给marsBox的dimensions样式。

由此可见,Property最大的特点是和时间相互关联,在不同的时间可以动态地返回不同的属性值。而Entity则可以感知这些Property的变化,在不同的时间驱动物体进行动态展示。

平台是数据驱动和 time-dynamic visualization,这些可都是仰仗Property属性机制来实现的。

当然,Property可不只是这么简单,以下再详细论述。

2.Property的分类

Cesium的Property不止有刚才示例代码中的SampleProperty,还有很多其他的类型。如果搜索一下Cesium的API文档,会有很多。如下图所示:

image.png

Cesium API文档搜索Property 我们简单分类一下

image.png

2.Property虚基类

Property是所有Property类型的虚基类。它定义了以下接口。

image.png

Property类型的公共接口

  • getValue 是一个方法,用来获取某个时间点的特定属性值。它有两个参数:第一个是time,用来传递一个时间点;第二个是result,用来存储属性值,当然也可以是undefined。这个result是Cesium的scratch机制,主要是用来避免频繁创建和销毁对象而导致内存碎片。Cesium就是通过调用getValue类似的一些函数来感知Property的变化的,当然这个方法我们在外部也是可以使用的。

  • isConstant 用来判断该属性是否会随时间变化,是一个布尔值。Cesium会通过这个变量来决定是否需要在场景更新的每一帧中都获取该属性的数值,从而来更新三维场景中的物体。如果isConstant为true,则只会获取一次数值,除非definitionChanged事件被触发。

  • definitionChanged 是一个事件,可以通过该事件,来监听该Property自身所发生的变化,比如数值发生修改。

  • equals 是一个方法,用来检测属性值是否相等。

2.基本Property类型

2. SampleProperty类

SampleProperty类是我们最早在上述示例中使用的就是它,用来通过给定多个不同时间点的Sample,然后在每两个时间点之间进行线性插值的一种Property。代码写法如下:

var property = new Cesium.SampledProperty(Cesium.Cartesian3)
property.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-25 08:00:00')), new Cesium.Cartesian3(400.0, 300.0, 100.0))
property.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-25 08:00:20')), new Cesium.Cartesian3(400.0, 300.0, 900.0))
//让盒子一直存在
// property.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-26 00:00:00')), new Cesium.Cartesian3(400.0, 300.0, 900.0))

marsBox.setStyle({ dimensions: property })

盒子逐渐变长

image.png

2. TimeIntervalCollectionProperty

TimeIntervalCollectionProperty用来指定各个具体的时间段的属性值,每个时间段内的属性值是恒定的,并不会发生变化,除非已经进入到下一个时间段。拿创建的盒子示例来说,表现出来的特点就是盒子尺寸的变化时跳跃式的。 效果如下:

image.png

代码如下:

var property = new Cesium.TimeIntervalCollectionProperty()
property.intervals.addInterval(
  Cesium.TimeInterval.fromIso8601({
    iso8601: '2017-08-25T00:00:00.00Z/2017-08-25T00:00:02.00Z',
    isStartIncluded: true,
    isStopIncluded: false,
    data: new Cesium.Cartesian3(400.0, 300.0, 200.0),
  })
)
property.intervals.addInterval(
  Cesium.TimeInterval.fromIso8601({
    iso8601: '2017-08-25T00:00:02.00Z/2017-08-25T00:00:04.00Z',
    isStartIncluded: true,
    isStopIncluded: false,
    data: new Cesium.Cartesian3(400.0, 300.0, 400.0),
  })
)
property.intervals.addInterval(
  Cesium.TimeInterval.fromIso8601({
    iso8601: '2017-08-25T00:00:04.00Z/2017-08-25T00:00:06.00Z',
    isStartIncluded: true,
    isStopIncluded: false,
    data: new Cesium.Cartesian3(400.0, 300.0, 500.0),
  })
)
property.intervals.addInterval(
  Cesium.TimeInterval.fromIso8601({
    iso8601: '2017-08-25T00:00:06.00Z/2017-08-25T00:00:08.00Z',
    isStartIncluded: true,
    isStopIncluded: true,
    data: new Cesium.Cartesian3(400.0, 300.0, 700.0),
  })
)

marsBox.setStyle({ dimensions: property })

2. ConstantProperty

通过对TimeIntervalCollectionProperty和SampleProperty的描述,读者应该基本了解Property的特点。 我们回过头来说下 ConstantProperty,其实这才是最常用的Property,也是平台默认使用的Property。

示例代码如下:

let dimensions = new Cesium.Cartesian3(400.0, 300.0, 200.0);
marsBox.setStyle({dimensions:dimensions}) 

以上代码貌似没有使用ConstantProperty,实际上他是等同于:

let dimensions = new ConstantProperty(new Cesium.Cartesian3(400.0, 300.0, 200.0));
marsBox.setStyle({dimensions:dimensions}) 

也就是Entity的 dimensions style样式并不是Cartesian3,而是一个Property。虽然我们赋值了一个Cartesian3,但是Cesium内部会隐晦地转化成了一个ConstantProperty。注意只会隐晦地转化成ConstantProperty,而不是SampleProperty,更不是TimeIntervalCollectionProperty。

虽然叫ConstantProperty,但是,这里Constant的意思并不是说这个Property不可改变,而是说它不会随时间发生变化。

举个例子,我们可以通过 property.getValue(viewer.clock.currentTime) 方法来获取某个时间点property的属性值。如果property是SampleProperty或者TimeIntervalCollectionProperty的话,不同的时间点,可能getValue出不同的数值。但是如果这个property是ConstantProperty,那么无论什么时间(getValue的第一个参数不起作用),最后返回的数值都是一样的。

但是不会随时间变化,并不代表不可改变。ConstantProperty还有一个setValue的方法,开发者可以通过调用它,来在适当的时候改变property的值。

比如,我可以通过点击按钮来修改ConstantProperty,代码如下:

marsBox.entityGraphic.dimensions.setValue(new Cesium.Cartesian3(400.0, 300.0, 700.0));

需要注意的是,虽然最终效果一样,但是以下两种写法的意义是不一样的。

//会创建一个新的ConstantProperty
marsBox.entityGraphic.dimensions = new Cesium.Cartesian3(400.0, 300.0, 200.0); 
//会修改原有的ConstantProperty的值。
marsBox.entityGraphic.dimensions.setValue(new Cesium.Cartesian3(400.0, 300.0, 700.0));

2. CompositeProperty

CompositeProperty的意思是组合的Property,可以把多种不同类型的ConstantProperty、SampleProperty、TimeIntervalCollectionProperty等Property组合在一起来操作。比如前一个时间段需要线性运动,后一段时间再跳跃式运动。则可以使用类似下面这段代码来实现。

// 1 sampledProperty
var sampledProperty = new Cesium.SampledProperty(Cesium.Cartesian3)
sampledProperty.addSample(Cesium.JulianDate.fromIso8601('2017-08-25T00:00:00.00Z'), new Cesium.Cartesian3(400.0, 300.0, 100.0))
sampledProperty.addSample(Cesium.JulianDate.fromIso8601('2017-08-25T00:00:10.00Z'), new Cesium.Cartesian3(400.0, 300.0, 500.0))

// 2 ticProperty
var ticProperty = new Cesium.TimeIntervalCollectionProperty()
ticProperty.intervals.addInterval(
  Cesium.TimeInterval.fromIso8601({
    iso8601: '2017-08-25T00:00:10.00Z/2017-08-25T00:00:12.00Z',
    isStartIncluded: true,
    isStopIncluded: false,
    data: new Cesium.Cartesian3(400.0, 300.0, 600.0),
  })
)
ticProperty.intervals.addInterval(
  Cesium.TimeInterval.fromIso8601({
    iso8601: '2017-08-25T00:00:12.00Z/2017-08-25T00:00:14.00Z',
    isStartIncluded: true,
    isStopIncluded: false,
    data: new Cesium.Cartesian3(400.0, 300.0, 700.0),
  })
)
ticProperty.intervals.addInterval(
  Cesium.TimeInterval.fromIso8601({
    iso8601: '2017-08-25T00:00:14.00Z/2017-08-25T00:00:16.00Z',
    isStartIncluded: true,
    isStopIncluded: false,
    data: new Cesium.Cartesian3(400.0, 300.0, 800.0),
  })
)
ticProperty.intervals.addInterval(
  Cesium.TimeInterval.fromIso8601({
    iso8601: '2017-08-25T00:00:16.00Z/2017-08-25T00:00:18.00Z',
    isStartIncluded: true,
    isStopIncluded: true,
    data: new Cesium.Cartesian3(400.0, 300.0, 900.0),
  })
)

// 3 compositeProperty
var compositeProperty = new Cesium.CompositeProperty()
compositeProperty.intervals.addInterval(
  Cesium.TimeInterval.fromIso8601({
    iso8601: '2017-08-25T00:00:00.00Z/2017-08-25T00:00:10.00Z',
    data: sampledProperty,
  })
)
compositeProperty.intervals.addInterval(
  Cesium.TimeInterval.fromIso8601({
    iso8601: '2017-08-25T00:00:10.00Z/2017-08-25T00:00:20.00Z',
    isStartIncluded: false,
    isStopIncluded: false,
    data: ticProperty,
  })
)

// 4 设置dimensions
marsBox.setStyle({ dimensions: compositeProperty })

最终实现的效果如下:

image.png

2. PositionProperty坐标属性

基于PositionProperty的类型有以下几种:

稍加留意,就会发现,和普通的Property相比,只是多了一个Position,所以用法上也大同小异,只不过他们是用来专门表示位置的。

2. SampledPositionProperty

SampledPositionProperty的用法,不多解释,直接看代码吧:

var property = new Cesium.SampledPositionProperty()
property.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-25 08:00:00')), Cesium.Cartesian3.fromDegrees(117.198461, 31.834956, 40.2))
property.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-25 08:00:20')), Cesium.Cartesian3.fromDegrees(117.231979, 31.833411, 35.6))

marsBox.position = property

效果如下:

image.png

SamplePropertySampledPositionProperty 有一个特有的方法:setInterpolationOptions,用来修改不同的插值方式。以下是以Cesium的Interpolation示例中的截图来说明他们的不同之处。

image.png

//线性插值
marsBox.position.setInterpolationOptions({
   interpolationDegree : 1,
   interpolationAlgorithm : Cesium.LinearApproximation
});

//Lagrange插值
marsBox.position.setInterpolationOptions({
   interpolationDegree : 5,
   interpolationAlgorithm : Cesium.LagrangePolynomialApproximation
});

//Hermite插值
marsBox.position.setInterpolationOptions({
   interpolationDegree : 2,
   interpolationAlgorithm : Cesium.HermitePolynomialApproximation
});

2. MaterialProperty

MaterialProperty是用来专门表示材质的Property,它对Property进行了扩展,增加了getType方法,用来获取材质类型。 效果如下:

image.png

MaterialProperty也是一个虚基类,派生类有:

使用上大同小异,我们以 ColorMaterialProperty来说明一下。

marsBox.setStyle({ material: new Cesium.ColorMaterialProperty(new Cesium.Color(0, 1, 0)) })
// 以上代码等同于
marsBox.setStyle({ material: new Cesium.Color(0, 1, 0) })

效果如下:

image.png

ColorMaterialProperty的动态变化

如果希望Color动起来的话,也是可以的。ColorMaterialProperty的内部有一个color属性,可以赋予一个SampledProperty来实现动态效果。

var colorProperty = new Cesium.SampledProperty(Cesium.Color)
colorProperty.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-25 08:00:00')), new Cesium.Color(0, 0, 1))
colorProperty.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-25 08:00:10')), new Cesium.Color(1, 1, 0))

marsBox.setStyle({ material: new Cesium.ColorMaterialProperty(colorProperty) })

效果如下:

image.png

2. CallbackProperty

CallbackProperty是自由度最高的一种Property,让用户通过自定义,回调函数,来返回需要的值。回调函数中,用户可以使用time来给定value,也可以以自己的方式给给定。

以下代码就是不通过time,自己手动调整dimension的示例。

var len = 100.0
var property = new Cesium.CallbackProperty(function (time, result) {
  result = result || new Cesium.Cartesian3(400.0, 300.0, 500.0)

  len += 3.0
  if (len > 900.0) {
    len = 100.0
  }

  result.x = 400.0
  result.y = 300.0
  result.z = len

  return result
}, false)

marsBox.setStyle({ dimensions: property })

效果如下,盒子逐渐变长:

image.png

2. ReferenceProperty

ReferenceProperty可以直接链接到别的对象的Property上,相当于引用,省得自己构建了。比如这里我创建了一个红色的盒子redBox,希望它和之前的蓝色盒子一起变大。那么可以使用以下代码:

以下代码就是不通过time,自己手动调整dimension的示例。

var collection = graphicLayer.dataSource.entities
let dimensions = new Cesium.ReferenceProperty(collection, marsBox.uuid, ['box', 'dimensions'])
redBox.setStyle({ dimensions: dimensions })

效果如下,盒子逐渐变长:

image.png

ReferenceProperty的使用

ReferenceProperty构造函数的参数 ReferenceProperty构造函数的参数有三个。第一个参数用来指定需要引用的对象所属的collection,如果没有自己专门创建EntityCollection的话,可以直接使用viewer.entities。第二个参数传递所指对象的id。第三个参数指定属性的位置的数组,如果是有层级的属性,可以依次写入。比如 ['billboard', 'scale'] 指定的是entity.billboard.scale 属性。当然还有其他设置方式,可以参见API文档

2. PropertyBag

PropertyBag虽然不是以Property结尾,但实际上也是一个Property。它的特点是可以包装一个对象(JS中的对象概念),该对象的每一个属性(JS中的属性概念),都可以作为一个动态的Property。

比如之前修改dimensions的话,dimensions是作为一个Cartesian3类型变量整体封装到Property中去的,如果我们只想修改dimensions的x。则可以使用PropertyBag来实现,代码如下:

var zp = new Cesium.SampledProperty(Number)
zp.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-25 08:00:00')), 100.0)
zp.addSample(Cesium.JulianDate.fromDate(new Date('2017-08-25 08:00:10')), 800.0)

let dimensions = new Cesium.PropertyBag({
  x: 400.0,
  y: 300.0,
  z: zp,
})
marsBox.setStyle({ dimensions: dimensions })

效果如下,盒子逐渐变长:

image.png

效果和sampleProperty类似,但是修改的只是dimensions的x。

2. PropertyArray

PropertyArray和上述的PropertyBag类似,只是其内部封装了一个数组而已。这里不再赘述。

2. VelocityOrientationProperty

VelocityOrientationProperty 用来Entity的position的位置变化,来计算出移动的方向,最后把速度方向输出成Orientation。Cesium自带的示例中有一个Interpolation中有其用法,不再赘述。

2. VelocityVectorProperty

VelocityVectorProperty 与上面的Property类似,把速度方向转成Vector。使用示例如下:

//计算演示的SampledPositionProperty轨迹
function getSampledPositionProperty(points) {
  let property = new Cesium.SampledPositionProperty()

  let start = map.clock.currentTime
  let positions = mars3d.LatLngArray.toCartesians(points)
  for (let i = 0; i < positions.length; i++) {
    let time = Cesium.JulianDate.addSeconds(start, i * 20, new Cesium.JulianDate())
    let position = positions[i]
    property.addSample(time, position)
  }
  return property
}

let propertyFJ = getSampledPositionProperty([
  [117.198461, 31.834956, 40.2],
  [117.231979, 31.833411, 35.6],
])
var graphic = new mars3d.graphic.BillboardEntity({
  position: propertyFJ,
  orientation: new Cesium.VelocityOrientationProperty(propertyFJ),
  style: {
    image: 'img/marker/huojian.svg',
    scale: 0.5,
    alignedAxis: new Cesium.VelocityVectorProperty(propertyFJ, true),
  },
})
graphicLayer.addGraphic(graphic)

可见图像的摆放方向和位置移动的方向保持一致。效果如下:

image.png