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

Feat/voxelchunks fadein #65

Merged
merged 17 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/lib/helpers/transition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class Transition {
private readonly startTimestamp: number = performance.now();

private duration: number;
private from: number;
private to: number;

public constructor(duration: number, from: number, to: number) {
this.duration = duration;
this.from = from;
this.to = to;
}

public get currentValue(): number {
const progress = this.progress;
return this.from * (1 - progress) + this.to * progress;
}

public isFinished(): boolean {
return this.progress === 1;
}

public get progress(): number {
const progress = (performance.now() - this.startTimestamp) / this.duration;
if (progress < 0) {
return 0;
} else if (progress > 1) {
return 1;
} else {
return progress;
}
}
}

export { Transition };
2 changes: 1 addition & 1 deletion src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export {
} from './terrain/voxelmap/viewer/autonomous/voxelmap-viewer-autonomous';
export {
EComputationMethod,
EComputationResult,
VoxelmapViewer,
type ComputationOptions,
type ComputationStatus,
type VoxelmapViewerOptions,
type VoxelsChunkData,
} from './terrain/voxelmap/viewer/simple/voxelmap-viewer';
Expand Down
2 changes: 1 addition & 1 deletion src/lib/terrain/terrain-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class TerrainViewer {
* Call this method before rendering.
* */
public update(): void {
this.voxelmapViewer.applyParameters();
this.voxelmapViewer.update();
this.updateHeightmap();
}

Expand Down
274 changes: 274 additions & 0 deletions src/lib/terrain/voxelmap/viewer/simple/stored-patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import { Transition } from '../../../../helpers/transition';
import type * as THREE from '../../../../libs/three-usage';
import { type PatchId } from '../../patch/patch-id';
import { EVoxelMaterialQuality } from '../../voxelsRenderable/voxels-material';
import { type VoxelsRenderable } from '../../voxelsRenderable/voxels-renderable';

enum EComputationResult {
SKIPPED = 'skipped',
CANCELLED = 'cancelled',
FINISHED = 'finished',
}

type UnknownFunc = () => unknown;
type AsyncTask<T> = () => Promise<T>;
type TaskRunner = {
run(task: UnknownFunc, onCancel: UnknownFunc): Promise<unknown>;
};

type AdaptativeQualityParameters = {
readonly distanceThreshold: number;
readonly cameraPosition: THREE.Vector3;
};

type Parameters = {
readonly parent: THREE.Object3D;
readonly id: PatchId;
readonly transitionTime: number;
};

class StoredPatch {
public readonly id: PatchId;
public readonly onVisibilityChange: VoidFunction[] = [];

private readonly transitionTime: number;
private readonly parent: THREE.Object3D;

private hasLatestData: boolean = false;
private latestComputationId: Symbol | null = null;
private computationResult: {
readonly voxelsRenderable: VoxelsRenderable | null;
} | null = null;

private disposed: boolean = false;
private shouldBeAttached: boolean = false;
private detachedSince: number | null = performance.now();
private transition: Transition | null = null;

private latestAdaptativeQualityParameters: AdaptativeQualityParameters | null = null;

public constructor(params: Parameters) {
this.parent = params.parent;
this.id = params.id;
this.transitionTime = params.transitionTime;
}

public update(): void {
const voxelRenderable = this.tryGetVoxelsRenderable();
if (voxelRenderable) {
if (this.transition) {
const wasFullyVisible = voxelRenderable.parameters.dissolveRatio === 0;
voxelRenderable.parameters.dissolveRatio = this.transition.currentValue;

if (this.transition.isFinished()) {
this.transition = null;

if (!this.shouldBeAttached) {
voxelRenderable.container.removeFromParent();
}
}

const isFullyVisible = voxelRenderable.parameters.dissolveRatio === 0;
if (wasFullyVisible !== isFullyVisible) {
this.notifyVisibilityChange();
}
}
}
}

public needsNewData(): boolean {
return !this.hasLatestData;
}

public flagAsObsolete(): void {
this.hasLatestData = false;
}

public setVisible(visible: boolean): void {
if (this.shouldBeAttached !== visible) {
this.shouldBeAttached = visible;

if (this.shouldBeAttached) {
this.detachedSince = null;
} else {
this.detachedSince = performance.now();
}

const voxelsRenderable = this.tryGetVoxelsRenderable();
if (voxelsRenderable) {
if (this.shouldBeAttached) {
this.parent.add(voxelsRenderable.container);
this.transitionToDissolved(false);
} else {
this.transitionToDissolved(true);
}
}
}
}

public isDetachedSince(): number | null {
return this.detachedSince;
}

public isMeshInScene(): boolean {
return !!this.tryGetVoxelsRenderable()?.container.parent;
}

public isAttached(): boolean {
if (!this.computationResult) {
return false;
}
const voxelsRenderable = this.computationResult.voxelsRenderable;
if (voxelsRenderable) {
return !!voxelsRenderable.container.parent && voxelsRenderable.parameters.dissolveRatio === 0;
} else {
// the patch was computed, but there is no mesh -> it is as if it was in the scene
return true;
}
}

public tryGetVoxelsRenderable(): VoxelsRenderable | null {
if (this.computationResult) {
return this.computationResult.voxelsRenderable;
}
return null;
}

public updateDisplayQuality(params: AdaptativeQualityParameters | null): void {
this.latestAdaptativeQualityParameters = params;

const voxelsRenerable = this.tryGetVoxelsRenderable();
if (voxelsRenerable) {
StoredPatch.enforceDisplayQuality(voxelsRenerable, this.latestAdaptativeQualityParameters);
}
}

public scheduleNewComputation(
computationTask: AsyncTask<VoxelsRenderable | null>,
taskRunner: TaskRunner
): Promise<EComputationResult> {
if (this.disposed) {
throw new Error(`Cannot compute disposed patch "${this.id.asString}".`);
}

const computationId = Symbol('stored-patch-computation');
this.latestComputationId = computationId;
this.hasLatestData = true;

return new Promise<EComputationResult>(resolve => {
const resolveAsCancelled = () => resolve(EComputationResult.CANCELLED);
const resolveAsFinished = () => resolve(EComputationResult.FINISHED);

taskRunner.run(async () => {
if (computationId !== this.latestComputationId) {
// a more recent computation has been requested before this one started
resolveAsCancelled();
return;
}

const computedVoxelsRenderable = await computationTask();

if (computationId !== this.latestComputationId) {
// a more recent computation has been requested while this one was running
if (computedVoxelsRenderable) {
computedVoxelsRenderable.dispose();
}
resolveAsCancelled();
return;
}
this.latestComputationId = null;

const wasMeshInScene = this.isMeshInScene();

if (this.computationResult) {
// we are overwriting a previous computation result
if (this.computationResult.voxelsRenderable) {
// properly remove the obsolete computation that we are overwriting
this.computationResult.voxelsRenderable.container.removeFromParent();
this.computationResult.voxelsRenderable.dispose();
}
}

this.computationResult = {
voxelsRenderable: computedVoxelsRenderable,
};

if (computedVoxelsRenderable) {
StoredPatch.enforceDisplayQuality(computedVoxelsRenderable, this.latestAdaptativeQualityParameters);
if (this.shouldBeAttached) {
this.parent.add(computedVoxelsRenderable.container);

if (!wasMeshInScene) {
this.transitionToDissolved(false);
}
}
}

resolveAsFinished();
}, resolveAsCancelled);
});
}

public cancelScheduledComputation(): void {
if (this.latestComputationId) {
this.latestComputationId = null;
this.hasLatestData = false;
}
}

public deleteComputationResults(): void {
if (this.computationResult) {
const voxelsRenderable = this.tryGetVoxelsRenderable();
if (voxelsRenderable) {
voxelsRenderable.container.removeFromParent();
voxelsRenderable.dispose();
}
this.computationResult = null;

this.hasLatestData = false;
}
}

public dispose(): void {
if (this.disposed) {
throw new Error(`Patch "${this.id.asString}" was disposed twice.`);
}
this.disposed = true;

this.cancelScheduledComputation();
this.deleteComputationResults();
}

private transitionToDissolved(dissolved: boolean): void {
let from: number;
if (this.transition) {
from = this.transition.currentValue;
} else {
from = dissolved ? 0 : 1;
}
const to = dissolved ? 1 : 0;

this.transition = new Transition(this.transitionTime * Math.abs(to - from), from, to);
}

private notifyVisibilityChange(): void {
for (const callback of this.onVisibilityChange) {
callback();
}
}

private static enforceDisplayQuality(voxelsRenderable: VoxelsRenderable, params: AdaptativeQualityParameters | null): void {
let quality = EVoxelMaterialQuality.HIGH;

if (params) {
const distance = voxelsRenderable.boundingBox.distanceToPoint(params.cameraPosition);
if (distance > params.distanceThreshold) {
quality = EVoxelMaterialQuality.LOW;
}
}

voxelsRenderable.quality = quality;
}
}

export { EComputationResult, StoredPatch, type AdaptativeQualityParameters };
Loading