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/props #60

Merged
merged 31 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f6928d0
test: dedicated scene for boards
piellardj Dec 16, 2024
16273bf
test: light up board cells when clicked
piellardj Dec 16, 2024
a35fb3b
test: add GUI for board testing
piellardj Dec 16, 2024
7ce6fff
refactor: extract method
piellardj Dec 29, 2024
c0e376d
refactor: factorize code
piellardj Dec 29, 2024
1ed5b60
test: base scene for grass
piellardj Dec 30, 2024
c6a922d
test: display player as a sphere
piellardj Jan 1, 2025
1b074e8
feat: player-reactive grass
piellardj Jan 1, 2025
ab03c84
fix: player position was sometimes no applied correctly
piellardj Jan 1, 2025
9504a1b
test: add 2d/3d grass toggle
piellardj Jan 1, 2025
c6bc8f4
test: use bluenoise for grass repartition
piellardj Jan 1, 2025
f30de06
test: add static rock props
piellardj Jan 1, 2025
f4cab5b
perf: handle transition GPU-side
piellardj Jan 1, 2025
f0a8637
refactor: renamings
piellardj Jan 1, 2025
78c16c7
feat: organize props per group
piellardj Jan 1, 2025
88be846
refactor: move code
piellardj Jan 1, 2025
0412948
fix: fix precision issues with player-grass interactions
piellardj Jan 1, 2025
3a3a51e
feat: support infinite number of props
piellardj Jan 2, 2025
dbf17a1
fix: correctly handle empty groups
piellardj Jan 2, 2025
c44e1ea
feat: add memory garbage collecting
piellardj Jan 2, 2025
031cae5
feat: memory statistics for PropsHandler
piellardj Jan 2, 2025
4db8495
feat: memory statistics for PropsBatch
piellardj Jan 2, 2025
bc1f6ad
test: dynamic & infinite grass generation
piellardj Jan 2, 2025
33f303a
fix: fix bug where some props were not displayed sometimes
piellardj Jan 2, 2025
d3dc151
refactor: simplification
piellardj Jan 2, 2025
5c4b5bb
fix: fix bad frustum culling from threejs
piellardj Jan 2, 2025
799355f
feat: new class PropsViewer
piellardj Jan 5, 2025
a6fcc63
test: add draw calls counter
piellardj Jan 5, 2025
e41b993
perf: don't draw batches that are out of view
piellardj Jan 5, 2025
3dad1fc
fix: fix threejs frustum culling
piellardj Jan 6, 2025
f69a728
feat: better statistics for memory, frustum culling etc
piellardj Jan 6, 2025
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
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"no-undef": "off",
"no-unused-vars": "off",
"no-use-before-define": "off", // handled by TS
"no-void": ["error", {
"allowAsStatement": true
}],
"@typescript-eslint/consistent-type-exports": [
"error",
{
Expand Down
12 changes: 1 addition & 11 deletions src/lib/effects/billboard/billboard-shader.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import { vec3ToString } from '../../helpers/string';
import { applyReplacements, vec3ToString } from '../../helpers/string';
import * as THREE from '../../libs/three-usage';

function applyReplacements(source: string, replacements: Record<string, string>): string {
let result = source;

for (const [source, replacement] of Object.entries(replacements)) {
result = result.replace(source, replacement);
}

return result;
}

type UniformType = 'sampler2D' | 'float' | 'vec2' | 'vec3' | 'vec4';
type UniformDefinition<T> = THREE.IUniform<T> & { readonly type: UniformType };

Expand Down
273 changes: 273 additions & 0 deletions src/lib/effects/props/props-batch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import { logger } from '../../helpers/logger';
import { copyMap } from '../../helpers/misc';
import { applyReplacements } from '../../helpers/string';
import * as THREE from '../../libs/three-usage';

type PropsMaterial = {
readonly material: THREE.MeshPhongMaterial;
readonly uniforms: {
uPlayerViewPosition: THREE.IUniform<THREE.Vector3>;
uViewRadius: THREE.IUniform<number>;
uViewRadiusMargin: THREE.IUniform<number>;
};
};

function buildNoiseTexture(resolution: number): THREE.DataTexture {
const textureWidth = resolution;
const textureHeight = resolution;
const textureData = new Uint8Array(textureWidth * textureHeight);

for (let i = 0; i < textureData.length; i++) {
textureData[i] = 250 * Math.random();
}

const texture = new THREE.DataTexture(textureData, textureWidth, textureHeight, THREE.RedFormat);
texture.needsUpdate = true;
return texture;
}

function customizeMaterial(phongMaterial: THREE.MeshPhongMaterial, playerReactive: boolean): PropsMaterial {
phongMaterial.customProgramCacheKey = () => `prop_phong_material`;

const noiseTextureSize = 64;
const noiseTexture = buildNoiseTexture(noiseTextureSize);
noiseTexture.wrapS = THREE.RepeatWrapping;
noiseTexture.wrapT = THREE.RepeatWrapping;
noiseTexture.magFilter = THREE.LinearFilter;

const customUniforms = {
uNoiseTexture: { value: noiseTexture },
uPlayerViewPosition: { value: new THREE.Vector3(Infinity, Infinity, Infinity) },
uViewRadius: { value: 10 },
uViewRadiusMargin: { value: 2 },
};

phongMaterial.onBeforeCompile = parameters => {
parameters.uniforms = {
...parameters.uniforms,
...customUniforms,
};

parameters.defines = parameters.defines || {};
const playerReactiveKey = 'PLAYER_REACTIVE';
if (playerReactive) {
parameters.defines[playerReactiveKey] = true;
}

parameters.vertexShader = applyReplacements(parameters.vertexShader, {
'void main() {': `
#ifdef ${playerReactiveKey}
uniform vec3 uPlayerViewPosition;
#endif

uniform float uViewRadius;
uniform float uViewRadiusMargin;

out float vDissolveRatio;

void main() {
`,
// https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/project_vertex.glsl.js
'#include <project_vertex>': `
vec4 mvPosition = vec4( transformed, 1.0 );

#ifdef USE_BATCHING
mvPosition = batchingMatrix * mvPosition;
#endif

#ifdef USE_INSTANCING
mvPosition = instanceMatrix * mvPosition;
#endif

float canBeDisplaced = step(0.2, mvPosition.y);

mvPosition = modelViewMatrix * mvPosition;

vec4 viewX = viewMatrix * vec4(1, 0, 0, 0);
vec4 viewZ = viewMatrix * vec4(0, 0, 1, 0);

#ifdef ${playerReactiveKey}
vec3 fromPlayer = mvPosition.xyz - uPlayerViewPosition;
float fromPlayerLength = length(fromPlayer) + 0.00001;
const float playerRadius = 0.6;
vec3 displacementViewspace = fromPlayer / fromPlayerLength * (playerRadius - fromPlayerLength)
* step(fromPlayerLength, playerRadius) * canBeDisplaced;
mvPosition.xyz +=
viewX.xyz * dot(displacementViewspace, viewX.xyz) +
viewZ.xyz * dot(displacementViewspace, viewZ.xyz);
#endif

vDissolveRatio = smoothstep(uViewRadius - uViewRadiusMargin, uViewRadius, length(mvPosition.xyz));

gl_Position = projectionMatrix * mvPosition;
`,
});

parameters.fragmentShader = applyReplacements(parameters.fragmentShader, {
'void main() {': `
uniform sampler2D uNoiseTexture;

in float vDissolveRatio;

void main() {
float noise = texture(uNoiseTexture, gl_FragCoord.xy / ${noiseTextureSize.toFixed(1)}).r;
if (noise < vDissolveRatio) {
discard;
}
`,
});
};
return {
material: phongMaterial,
uniforms: customUniforms,
};
}

type PropsBatchStatistics = {
instancesCapacity: number;
instancesUsed: number;
groupsCount: number;
boundingSphereRadius: number;
buffersSizeInBytes: number;
};

type Paramerers = {
readonly maxInstancesCount: number;
readonly reactToPlayer: boolean;
readonly bufferGeometry: THREE.BufferGeometry;
readonly material: THREE.MeshPhongMaterial;
};

type GroupDefinition = {
readonly startIndex: number;
readonly count: number;
};

class PropsBatch {
public get container(): THREE.InstancedMesh {
return this.instancedMesh;
}

public readonly playerViewPosition = new THREE.Vector3();

private readonly maxInstancesCount: number;
private readonly instancedMesh: THREE.InstancedMesh;

private readonly material: PropsMaterial;
private readonly groupsDefinitions: Map<string, GroupDefinition>;

public constructor(params: Paramerers) {
this.maxInstancesCount = params.maxInstancesCount;

this.material = customizeMaterial(params.material, params.reactToPlayer);
this.playerViewPosition = this.material.uniforms.uPlayerViewPosition.value;
this.groupsDefinitions = new Map();

this.instancedMesh = new THREE.InstancedMesh(params.bufferGeometry, this.material.material, this.maxInstancesCount);
this.instancedMesh.count = 0;
}

public setInstancesGroup(groupName: string, matricesList: ReadonlyArray<THREE.Matrix4>): void {
if (this.groupsDefinitions.has(groupName)) {
this.groupsDefinitions.delete(groupName);
this.reorderMatricesBuffer();
}

if (matricesList.length === 0) {
return;
}

if (matricesList.length > this.spareInstancesLeft) {
throw new Error(
`Props batch don't have enough space to store "${matricesList.length}" more instances ("${this.spareInstancesLeft}" left)`
);
}

const newGroup: GroupDefinition = {
startIndex: this.instancedMesh.count,
count: matricesList.length,
};
this.groupsDefinitions.set(groupName, newGroup);
matricesList.forEach((matrix: THREE.Matrix4, index: number) => {
this.instancedMesh.setMatrixAt(newGroup.startIndex + index, matrix);
});
this.instancedMesh.instanceMatrix.needsUpdate = true;
this.instancedMesh.count += matricesList.length;
this.updateFrustumCulling();
}

public deleteInstancesGroup(groupName: string): void {
if (this.groupsDefinitions.has(groupName)) {
this.groupsDefinitions.delete(groupName);
this.reorderMatricesBuffer();
this.updateFrustumCulling();
} else {
logger.warn(`Unknown props batch group "${groupName}".`);
}
}

public setViewDistance(distance: number): void {
this.material.uniforms.uViewRadius.value = distance;
}

public setViewDistanceMargin(margin: number): void {
this.material.uniforms.uViewRadiusMargin.value = margin;
}

public get spareInstancesLeft(): number {
return this.maxInstancesCount - this.instancedMesh.count;
}

public dispose(): void {
this.instancedMesh.geometry.dispose();
}

public getStatistics(): PropsBatchStatistics {
let buffersSizeInBytes = 0;
for (const attributeBuffer of Object.values(this.instancedMesh.geometry.attributes)) {
buffersSizeInBytes += attributeBuffer.array.byteLength;
}
buffersSizeInBytes += this.instancedMesh.instanceColor?.array.byteLength ?? 0;
buffersSizeInBytes += this.instancedMesh.instanceMatrix?.array.byteLength ?? 0;

return {
instancesCapacity: this.maxInstancesCount,
instancesUsed: this.instancedMesh.count,
groupsCount: this.groupsDefinitions.size,
boundingSphereRadius: this.instancedMesh.boundingSphere?.radius ?? Infinity,
buffersSizeInBytes,
};
}

private reorderMatricesBuffer(): void {
const reorderedMatrices = new Float32Array(this.instancedMesh.instanceMatrix.array.length);

let instancesCount = 0;
const newGroupDefinitions = new Map<string, GroupDefinition>();
for (const [groupName, groupDefinition] of this.groupsDefinitions.entries()) {
const newGroupDefinition: GroupDefinition = {
startIndex: instancesCount,
count: groupDefinition.count,
};
newGroupDefinitions.set(groupName, newGroupDefinition);
instancesCount += groupDefinition.count;

for (let iM = 0; iM < groupDefinition.count; iM++) {
const oldMatrixStart = 16 * (groupDefinition.startIndex + iM);
const matrix = this.instancedMesh.instanceMatrix.array.subarray(oldMatrixStart, oldMatrixStart + 16);
reorderedMatrices.set(matrix, 16 * (newGroupDefinition.startIndex + iM));
}
}
copyMap(newGroupDefinitions, this.groupsDefinitions);

this.instancedMesh.instanceMatrix.array.set(reorderedMatrices.subarray(0, 16 * instancesCount), 0);
this.instancedMesh.count = instancesCount;
}

private updateFrustumCulling(): void {
this.instancedMesh.computeBoundingBox();
this.instancedMesh.computeBoundingSphere();
}
}

export { PropsBatch };
Loading
Loading