buildParityScene function

({CameraNode camera, Scene scene}) buildParityScene(
  1. GraphicsDevice device, {
  2. ParityScene which = ParityScene.plain,
})

Implementation

({Scene scene, CameraNode camera}) buildParityScene(
  GraphicsDevice device, {
  ParityScene which = ParityScene.plain,
}) {
  // A room rather than two spheres, and so it is built somewhere else entirely
  // rather than by threading conditions through the code below.
  if (which == ParityScene.torchNearWall ||
      which == ParityScene.torchesRunningOut) {
    return _buildTorchNearWall(
      device,
      torches: which == ParityScene.torchesRunningOut ? 6 : 1,
    );
  }

  final scene = Scene(name: 'parity');

  final big = DeviceMesh.upload(
    device,
    const SphereShape(radius: 1.0, segments: 32, rings: 16).build(),
  );
  scene.root.add(
    MeshNode(
      big,
      Material(
        name: 'big',
        baseColor: Vector4(0.85, 0.25, 0.15, 1.0),
        // Physical only where the fixture is about what a surface reflects:
        // Lambert has no response to an environment at all, so an IBL scene
        // shaded that way would compare two flat colours and pass whatever the
        // cube contained.
        lighting: which == ParityScene.imageBasedLighting
            ? LightingModel.pbr
            : LightingModel.lambert,
        metallic: 0.9,
        roughness: 0.25,
      ),
      name: 'big',
    ),
  );

  // Up and to the right, and small enough that its silhouette is unmistakable.
  // This is the asymmetry: a mirrored frame puts it down and to the left, and a
  // comparison that only looked at brightness would not care.
  final small = DeviceMesh.upload(
    device,
    const SphereShape(radius: 0.35, segments: 16, rings: 8).build(),
  );
  scene.root.add(
    MeshNode(
        small,
        Material(
          name: 'small',
          baseColor: Vector4(0.2, 0.5, 0.9, 1.0),
          lighting: which == ParityScene.imageBasedLighting
              ? LightingModel.pbr
              : LightingModel.lambert,
          // Rougher than the big one, so the two read different levels of the
          // chain. A fixture where every surface is a mirror tests level zero.
          metallic: 1.0,
          roughness: 0.55,
        ),
        name: 'small',
      )
      ..setPosition(1.1, 1.0, 0.4)
      // **One static caster, so that the second cube atlas is not empty.**
      // A point light has two — the movers, redrawn every frame, and the things
      // that never move, drawn once — and `PointShadowDistance` samples both.
      // Every fixture here left both spheres dynamic, so the static atlas was
      // white in every run on every backend, and the half of the lookup that
      // reads it was covered by nothing at all.
      //
      // The large sphere stays dynamic on purpose: a fixture where one atlas is
      // empty tests one atlas, whichever one it is.
      ..shadowIsStatic =
          which == ParityScene.pointShadow ||
          which == ParityScene.pointShadowMap ||
          which == ParityScene.pointShadowStaticMap,
  );

  // A floor for anything that casts, and only then: an unlit scene with a
  // floor compares differently for a reason that has nothing to do with the
  // feature under test.
  if (which == ParityScene.directionalShadow ||
      which == ParityScene.pointShadow ||
      which == ParityScene.pointShadowMap ||
      which == ParityScene.pointShadowStaticMap) {
    scene.root.add(
      MeshNode(
        DeviceMesh.upload(
          device,
          const PlaneShape(width: 8.0, depth: 8.0).build(),
        ),
        Material(
          name: 'floor',
          baseColor: Vector4(0.55, 0.55, 0.6, 1.0),
          lighting: LightingModel.lambert,
        ),
        name: 'floor',
      )..setPosition(0.0, -1.4, 0.0),
    );
  }

  switch (which) {
    // Built and returned at the top of this function, before any of the
    // spheres above exist. Named here rather than left to a default, so that
    // adding a fixture goes on failing to compile until somebody says where
    // its light comes from.
    case ParityScene.torchNearWall:
    case ParityScene.torchesRunningOut:
      break;

    case ParityScene.plain:
    case ParityScene.bloom:
      // Above and to the right, so the lit side of both spheres is up.
      scene.root.add(
        LightNode(name: 'key', type: LightType.directional)
          ..intensity = 3.5
          ..setPosition(1.5, 3.0, 2.0)
          ..lookAt(Vector3.zero()),
      );

    case ParityScene.directionalShadow:
      // Oblique rather than overhead. A sun straight above drops the shadow
      // under the sphere, where the sphere covers it — still cast, never seen,
      // and a comparison that cannot see it cannot disagree about it.
      scene.root.add(
        LightNode(name: 'sun', type: LightType.directional)
          ..intensity = 4.0
          ..castsShadow = true
          ..setPosition(2.5, 3.0, 1.5)
          ..lookAt(Vector3.zero()),
      );

    case ParityScene.pointShadow:
    case ParityScene.pointShadowMap:
    case ParityScene.pointShadowStaticMap:
      scene.root.add(
        LightNode(name: 'lamp', type: LightType.point)
          ..intensity = 12.0
          ..range = 12.0
          ..castsShadow = true
          ..setPosition(2.2, 2.6, 1.8),
      );

    case ParityScene.sky:
    case ParityScene.look:
      scene.root.add(
        LightNode(name: 'key', type: LightType.directional)
          ..intensity = 3.5
          ..setPosition(1.5, 3.0, 2.0)
          ..lookAt(Vector3.zero()),
      );

    case ParityScene.imageBasedLighting:
      // **No direct light at all.** Everything in this picture came out of the
      // cube, so a backend that binds no environment draws two silhouettes and
      // the number says so, rather than being carried by a key light.
      final environment = EnvironmentMap.fromSky(device, _paritySky);
      if (environment != null) {
        scene.environment = environment.texture;
        scene.environmentLevels = environment.levels;
      }
      scene.ambientIntensity = 1.0;
  }

  final camera = CameraNode(name: 'eye')
    ..setPosition(0.0, 0.4, 4.2)
    ..lookAt(Vector3(0.0, 0.2, 0.0));
  scene.root.add(camera);

  return (scene: scene, camera: camera);
}