Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support view-independent decoration graphics #3007

Merged
merged 11 commits into from
Jan 10, 2022
1 change: 1 addition & 0 deletions common/api/core-frontend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4033,6 +4033,7 @@ export interface GraphicBuilderOptions {
placement?: Transform;
preserveOrder?: boolean;
type: GraphicType;
viewIndependentOrigin?: Point3d;
wantNormals?: boolean;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-frontend",
"comment": "Add support for view-independent decoration graphics.",
"type": "none"
}
],
"packageName": "@itwin/core-frontend"
}
904 changes: 452 additions & 452 deletions core/frontend/src/ViewContext.ts

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions core/frontend/src/render/GraphicBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ export interface GraphicBuilderOptions {
* @note Edges will tend to z-fight with their surfaces unless the graphic is [[pickable]].
*/
generateEdges?: boolean;

/** If defined, specifies a point about which the graphic will rotate such that it always faces the viewer.
* This can be particular useful for planar regions to create a billboarding effect - e.g., to implement [[Marker]]-like WebGL decorations.
* @note This has no effect for graphics displayed in a 2d view.
*/
viewIndependentOrigin?: Point3d;
bbastings marked this conversation as resolved.
Show resolved Hide resolved
}

/** Options for creating a [[GraphicBuilder]] to produce a [[RenderGraphic]] to be displayed in a specific [[Viewport]].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class GeometryAccumulator {
private _transform: Transform;
private _surfacesOnly: boolean;
private readonly _analysisDisplacement?: AnalysisStyleDisplacement;
private readonly _viewIndependentOrigin?: Point3d;

public readonly tileRange: Range3d;
public readonly geometries: GeometryList = new GeometryList();
Expand All @@ -41,12 +42,14 @@ export class GeometryAccumulator {
transform?: Transform;
tileRange?: Range3d;
analysisStyleDisplacement?: AnalysisStyleDisplacement;
viewIndependentOrigin?: Point3d;
}) {
this.system = options?.system ?? IModelApp.renderSystem;
this.tileRange = options?.tileRange ?? Range3d.createNull();
this._surfacesOnly = true === options?.surfacesOnly;
this._transform = options?.transform ?? Transform.createIdentity();
this._analysisDisplacement = options?.analysisStyleDisplacement;
this._viewIndependentOrigin = options?.viewIndependentOrigin;
}

private getPrimitiveRange(geom: PrimitiveGeometryType): Range3d | undefined {
Expand Down Expand Up @@ -190,7 +193,7 @@ export class GeometryAccumulator {

verts.params.origin.setZero();

const graphic = mesh.getGraphics(args, this.system);
const graphic = mesh.getGraphics(args, this.system, this._viewIndependentOrigin);
if (undefined !== graphic)
branch.add(graphic);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export abstract class GeometryListBuilder extends GraphicBuilder {
system,
transform: accumulatorTransform,
analysisStyleDisplacement: this.analysisStyle?.displacement,
viewIndependentOrigin: options.viewIndependentOrigin,
});
}

Expand Down
8 changes: 4 additions & 4 deletions core/frontend/src/render/primitives/mesh/MeshPrimitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { assert } from "@itwin/core-bentley";
import { AuxChannel, AuxChannelData, Point2d, Range3d } from "@itwin/core-geometry";
import { AuxChannel, AuxChannelData, Point2d, Point3d, Range3d } from "@itwin/core-geometry";
import {
ColorIndex, EdgeArgs, Feature, FeatureIndex, FeatureIndexType, FeatureTable, FillFlags, LinePixels, MeshEdges, MeshPolyline, MeshPolylineList,
OctEncodedNormal, PolylineData, PolylineEdgeArgs, PolylineFlags, QParams3d, QPoint3dList, RenderMaterial, RenderTexture, SilhouetteEdgeArgs,
Expand Down Expand Up @@ -284,12 +284,12 @@ export class Mesh {
this.features.toFeatureIndex(index);
}

public getGraphics(args: MeshGraphicArgs, system: RenderSystem, instances?: InstancedGraphicParams): RenderGraphic | undefined {
public getGraphics(args: MeshGraphicArgs, system: RenderSystem, instancesOrViewIndependentOrigin?: InstancedGraphicParams | Point3d): RenderGraphic | undefined {
if (undefined !== this.triangles && this.triangles.length !== 0) {
if (args.meshArgs.init(this))
return system.createTriMesh(args.meshArgs, instances);
return system.createTriMesh(args.meshArgs, instancesOrViewIndependentOrigin);
} else if (undefined !== this.polylines && this.polylines.length !== 0 && args.polylineArgs.init(this)) {
return system.createIndexedPolylines(args.polylineArgs, instances);
return system.createIndexedPolylines(args.polylineArgs, instancesOrViewIndependentOrigin);
}

return undefined;
Expand Down
16 changes: 7 additions & 9 deletions core/frontend/src/test/ExpectColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,18 @@ export function expectColors(viewport: ScreenViewport, expected: ColorDef[], vie
expect(buf).not.to.be.undefined;

const u32 = new Uint32Array(buf.data.buffer);
const values = new Set<number>();
for (const rgba of u32)
values.add(rgba);

expect(values.size).to.equal(expected.length);

for (const rgba of values) {
const actualColors = new Set<ColorDef>();
for (const rgba of u32) {
const r = ((rgba & 0x000000ff) >>> 0x00) >>> 0;
const g = ((rgba & 0x0000ff00) >>> 0x08) >>> 0;
const b = ((rgba & 0x00ff0000) >>> 0x10) >>> 0;
const a = ((rgba & 0xff000000) >>> 0x18) >>> 0;
const actualColor = ColorDef.from(r, g, b, 0xff - a);
expect(expected.findIndex((x) => x.tbgr === actualColor.tbgr)).least(0);
actualColors.add(ColorDef.from(r, g, b, 0xff - a));
}

const expectedTbgr = expected.map((x) => x.tbgr.toString(16)).sort();
const actualTbgr = Array.from(actualColors).map((x) => x.tbgr.toString(16)).sort();
expect(actualTbgr).to.deep.equal(expectedTbgr);
}

/** A viewport-color-checking function for tests. Tests for the presence of a list of any unexpected colors in the entire viewport or specified ViewRect. If any of the colors are found, this function expects them not to be found and will fail the test.
Expand Down
60 changes: 48 additions & 12 deletions core/frontend/src/test/TestDecorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,78 @@ import { IModelApp } from "../IModelApp";
import { DecorateContext } from "../ViewContext";
import { ScreenViewport } from "../Viewport";
import { GraphicType, PickableGraphicOptions } from "../render/GraphicBuilder";
import {GraphicBranch} from "../core-frontend";

/** A simple configurable box decorator for tests.
* @internal
*/
export class BoxDecorator {
public constructor(public readonly vp: ScreenViewport, public readonly color: ColorDef, public readonly pickable?: PickableGraphicOptions, public readonly placement?: Transform, private _shapePoints?: Point3d[]) {
IModelApp.viewManager.addDecorator(this);
}
public viewport: ScreenViewport;
public color: ColorDef;
public pickable?: PickableGraphicOptions;
public placement?: Transform;
public points: Point3d[];
public viewIndependentOrigin?: Point3d;
public branchTransform?: Transform;

public drop() {
IModelApp.viewManager.dropDecorator(this);
}
public constructor(options: {
viewport: ScreenViewport;
color: ColorDef;
pickable?: PickableGraphicOptions;
placement?: Transform;
points?: Point3d[];
viewIndependentOrigin?: Point3d;
branchTransform?: Transform;
}) {
this.viewport = options.viewport;
this.color = options.color;
this.pickable = options.pickable;
this.placement = options.placement;
this.viewIndependentOrigin = options.viewIndependentOrigin;
this.branchTransform = options.branchTransform;

public decorate(context: DecorateContext): void {
if (undefined === this._shapePoints) {
if (options.points) {
this.points = options.points;
} else {
const w = 0.5;
const h = 0.5;
this._shapePoints = [
this.points = [
new Point3d(0, 0, 0),
new Point3d(w, 0, 0),
new Point3d(w, h, 0),
new Point3d(0, h, 0),
new Point3d(0, 0, 0),
];
this.vp.npcToWorldArray(this._shapePoints);

this.viewport.npcToWorldArray(this.points);
}

IModelApp.viewManager.addDecorator(this);
}

public drop() {
IModelApp.viewManager.dropDecorator(this);
}

public decorate(context: DecorateContext): void {
const builder = context.createGraphic({
placement: this.placement,
type: GraphicType.Scene,
pickable: this.pickable,
viewIndependentOrigin: this.viewIndependentOrigin,
});

builder.setSymbology(this.color, this.color, 1);
builder.addShape(this._shapePoints);
context.addDecorationFromBuilder(builder);
builder.addShape(this.points);

let graphic = builder.finish();
if (this.branchTransform) {
const branch = new GraphicBranch();
branch.add(graphic);
graphic = context.createBranch(branch, this.branchTransform);
}

context.addDecoration(GraphicType.Scene, graphic);
}
}

Expand Down
39 changes: 29 additions & 10 deletions core/frontend/src/test/render/webgl/Decorations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { createBlankConnection } from "../../createBlankConnection";
import { BoxDecorator, SphereDecorator } from "../../TestDecorators";
import { expectColors } from "../../ExpectColors";
import { ViewRect } from "../../../ViewRect";
import { ViewState } from "../../../ViewState";
import { StandardViewId } from "../../../StandardView";

describe("Decorations", () => {
let imodel: IModelConnection;
Expand All @@ -21,13 +23,11 @@ describe("Decorations", () => {
let boxDecLocRect: ViewRect;
let sphereDecBgLocRect: ViewRect;

const w = 0.5;
const h = 0.5;
const shapePoints = [
new Point3d(0, 0, 0),
new Point3d(w, 0, 0),
new Point3d(w, h, 0),
new Point3d(0, h, 0),
new Point3d(0.5, 0, 0),
new Point3d(0.5, 0.5, 0),
new Point3d(0, 0.5, 0),
new Point3d(0, 0, 0),
];

Expand All @@ -52,13 +52,13 @@ describe("Decorations", () => {
viewport = ScreenViewport.create(div, view);
width = viewport.viewRect.width;
height = viewport.viewRect.height;
boxDecLocRect = new ViewRect(0, 0, 1, 1);
boxDecLocRect = new ViewRect(0, 0, width / 2, height / 2);
sphereDecBgLocRect = new ViewRect(width - 2, height / 2 + 128, width - 2 + 1, height / 2 + 128 + 1);
});

afterEach(() => {
viewport.dispose();
for (const decorator of IModelApp.viewManager.decorators.filter((x) => x instanceof BoxDecorator))
for (const decorator of IModelApp.viewManager.decorators.filter((x) => x instanceof BoxDecorator || x instanceof SphereDecorator))
IModelApp.viewManager.dropDecorator(decorator);
});

Expand All @@ -69,16 +69,16 @@ describe("Decorations", () => {
});

it("draws box decoration in expected location", () => {
const dec = new BoxDecorator(viewport, ColorDef.red, undefined, undefined, shapePoints);
const dec = new BoxDecorator({ viewport, color: ColorDef.red, points: shapePoints });
expectColors(viewport, [dec.color, viewport.view.displayStyle.backgroundColor]); // are both the decorator and background rendering?
expectColors(viewport, [dec.color], boxDecLocRect); // is decorator rendering at expected location?
dec.drop();
}).timeout(20000); // macOS is slow.

it("draws box decoration in graphic-builder-transformed location", () => {
const dec = new BoxDecorator(viewport, ColorDef.red, undefined, Transform.createTranslationXYZ(0.25, 0.25), shapePoints);
const dec = new BoxDecorator({ viewport, color: ColorDef.red, placement: Transform.createTranslationXYZ(0.25, 0.25), points: shapePoints });
expectColors(viewport, [dec.color, viewport.view.displayStyle.backgroundColor]); // are both the decorator and background rendering?
expectColors(viewport, [viewport.view.displayStyle.backgroundColor], boxDecLocRect); // background should render where the decorator would have been without transform.
expectColors(viewport, [viewport.view.displayStyle.backgroundColor], new ViewRect(0, 0, 10, 10)); // background should render where the decorator would have been without transform.
dec.drop();
}).timeout(20000); // macOS is slow.

Expand All @@ -95,4 +95,23 @@ describe("Decorations", () => {
expectColors(viewport, [dec.color], sphereDecBgLocRect); // when sphere is transformed, this location should contain the sphere
dec.drop();
}).timeout(20000); // macOS is slow.

// ###TODO: Viewport.readImage is broken. It accepts a ViewRect which defines the origin at the top-left, and passes it to gl.readPixels, which
// defines the origin at the bottom-left. Update these tests after fixing that.
it("rotates about view-independent origin", () => {
const viewIndependentOrigin = new Point3d(0.5, 0.5, 0);
const dec = new BoxDecorator({ viewport, color: ColorDef.red, points: shapePoints, viewIndependentOrigin });
expectColors(viewport, [dec.color, viewport.view.displayStyle.backgroundColor]);
expectColors(viewport, [dec.color], boxDecLocRect);

viewport.view.setRotationAboutPoint(ViewState.getStandardViewMatrix(StandardViewId.Bottom), viewIndependentOrigin);
viewport.synchWithView();
expectColors(viewport, [dec.color, viewport.view.displayStyle.backgroundColor]);
expectColors(viewport, [dec.color], boxDecLocRect);

viewport.view.setRotationAboutPoint(ViewState.getStandardViewMatrix(StandardViewId.Front), viewIndependentOrigin);
viewport.synchWithView();
expectColors(viewport, [dec.color, viewport.view.displayStyle.backgroundColor]);
expectColors(viewport, [dec.color], boxDecLocRect);
}).timeout(20000);
});
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,14 @@ describe("Pickable graphic", () => {
}

it("is pickable", () => {
const dec = new BoxDecorator(viewport, ColorDef.red, { id: "0x123", locateOnly: false });
const dec = new BoxDecorator({ viewport, color: ColorDef.red, pickable: { id: "0x123", locateOnly: false } });
expectColors(viewport, [dec.color, viewport.view.displayStyle.backgroundColor]);
expect(dec.pickable).to.not.be.undefined;
expectIds([dec.pickable!.id]);
}).timeout(20000); // macOS is slow.

it("optionally draws only for pick", () => {
const dec = new BoxDecorator(viewport, ColorDef.blue, { id: "0x456", locateOnly: true });
const dec = new BoxDecorator({ viewport, color: ColorDef.blue, pickable: { id: "0x456", locateOnly: true } });
expectColors(viewport, [viewport.view.displayStyle.backgroundColor]);
expect(dec.pickable).to.not.be.undefined;
expectIds([dec.pickable!.id]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ class GeometryDecorator {
public readonly useCachedDecorations = true;
private readonly _iModel: IModelConnection;
private readonly _decorators = new Map<string, (builder: GraphicBuilder) => void>();
private readonly _viewIndependentOrigin?: Point3d;

public constructor(viewport: Viewport) {
public constructor(viewport: Viewport, viewIndependentOrigin?: Point3d) {
this._iModel = viewport.iModel;
this._viewIndependentOrigin = viewIndependentOrigin;

this.addSphere(0);
this.addBox(2);
Expand All @@ -30,7 +32,11 @@ class GeometryDecorator {
let colorIndex = 0;
const branch = new GraphicBranch();
for (const [key, value] of this._decorators) {
const builder = context.createGraphicBuilder(GraphicType.Scene, undefined, key);
const builder = context.createGraphic({
type: GraphicType.Scene,
pickable: { id: key },
viewIndependentOrigin: this._viewIndependentOrigin,
});

const color = colors[colorIndex++];
if (colorIndex >= colors.length)
Expand Down Expand Up @@ -75,7 +81,8 @@ class GeometryDecorator {
}

export function openDecorationGeometryExample(viewer: Viewer): void {
IModelApp.viewManager.addDecorator(new GeometryDecorator(viewer.viewport));
const viewIndependentOrigin = undefined; // new Point3d(4, 0, 0) -- uncomment for testing.
IModelApp.viewManager.addDecorator(new GeometryDecorator(viewer.viewport, viewIndependentOrigin));

assert(viewer.viewport.view.is3d());
viewer.viewport.setStandardRotation(StandardViewId.Iso);
Expand Down