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/customizable texture #49

Merged
merged 4 commits into from
Oct 2, 2024
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
7 changes: 2 additions & 5 deletions src/lib/effects/billboard/gpu/gpu-textures-state.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createFullscreenQuad } from '../../../helpers/fullscreen-quad';
import * as THREE from '../../../libs/three-usage';

type UniformType = 'sampler2D' | 'float' | 'vec2' | 'vec3' | 'vec4';
Expand Down Expand Up @@ -38,11 +39,7 @@ class GpuTexturesState {

this.textureNames = params.textureNames;

const fullscreenQuadGeometry = new THREE.BufferGeometry();
fullscreenQuadGeometry.setAttribute('aPosition', new THREE.Float32BufferAttribute([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1], 2));
fullscreenQuadGeometry.setDrawRange(0, 6);
this.fullscreenQuad = new THREE.Mesh(fullscreenQuadGeometry);
this.fullscreenQuad.frustumCulled = false;
this.fullscreenQuad = createFullscreenQuad('aPosition');

const vertexShader = `
in vec2 aPosition;
Expand Down
168 changes: 168 additions & 0 deletions src/lib/helpers/customizable-texture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import * as THREE from '../libs/three-usage';

import { createFullscreenQuad } from './fullscreen-quad';
import { logger } from './logger';

type Parameters = {
readonly baseTexture: THREE.Texture;
readonly additionalTextures: ReadonlyMap<string, THREE.Texture>;
};

type TextureLayer = {
readonly texture: THREE.Texture;
readonly color: THREE.Color;
};

class CustomizableTexture {
public readonly texture: THREE.Texture;

private readonly baseTexture: THREE.Texture;
private readonly layers: ReadonlyMap<string, TextureLayer>;

private readonly renderTarget: THREE.WebGLRenderTarget;
private readonly fakeCamera = new THREE.PerspectiveCamera();
private readonly fullscreenQuad = createFullscreenQuad('aPosition');
private readonly applyLayer: {
readonly shader: THREE.RawShaderMaterial;
readonly uniforms: {
readonly layer: THREE.IUniform<THREE.Texture | null>;
readonly color: THREE.IUniform<THREE.Color>;
readonly flipY: THREE.IUniform<number>;
};
};

public constructor(params: Parameters) {
this.baseTexture = params.baseTexture;

this.renderTarget = new THREE.WebGLRenderTarget(this.baseTexture.image.width, this.baseTexture.image.height, {
wrapS: this.baseTexture.wrapS,
wrapT: this.baseTexture.wrapT,
magFilter: this.baseTexture.magFilter,
// minFilter: this.baseTexture.minFilter,
depthBuffer: false,
});
const texture = this.renderTarget.textures[0];
if (!texture) {
throw new Error(`Cannot get texture from rendertarget`);
}
this.texture = texture;

const layers = new Map<string, TextureLayer>();
for (const [name, texture] of params.additionalTextures.entries()) {
if (texture.image.width !== this.renderTarget.width || texture.image.height !== this.renderTarget.height) {
logger.warn(
`Invalid texture size: expected "${this.renderTarget.width}x${this.renderTarget.height}" but received "${texture.image.width}x${texture.image.height}".`
);
}
layers.set(name, { texture, color: new THREE.Color(0xffffff) });
}
this.layers = layers;

const uniforms = {
layer: { value: null },
color: { value: new THREE.Color(0xffffff) },
flipY: { value: 0 },
};

const shader = new THREE.RawShaderMaterial({
glslVersion: '300 es',
depthTest: false,
blending: THREE.CustomBlending,
blendSrc: THREE.SrcAlphaFactor,
blendDst: THREE.OneMinusSrcAlphaFactor,
blendSrcAlpha: THREE.ZeroFactor,
blendDstAlpha: THREE.OneFactor,
uniforms: {
uLayerTexture: uniforms.layer,
uLayerColor: uniforms.color,
uFlipY: uniforms.flipY,
},
vertexShader: `
uniform float uFlipY;

in vec2 aPosition;

out vec2 vUv;

void main() {
gl_Position = vec4(2.0 * aPosition - 1.0, 0, 1);
vUv = vec2(
aPosition.x,
mix(aPosition.y, 1.0 - aPosition.y, uFlipY)
);
}`,
fragmentShader: `
precision mediump float;

uniform sampler2D uLayerTexture;
uniform vec3 uLayerColor;

in vec2 vUv;

layout(location = 0) out vec4 fragColor;

void main() {
vec4 sampled = texture(uLayerTexture, vUv);
if (sampled.a < 0.5) discard;
sampled.rgb *= uLayerColor;
fragColor = sampled;
}
`,
});
this.fullscreenQuad.material = shader;

this.applyLayer = { shader, uniforms };
}

public setLayerColor(layerName: string, color: THREE.Color): void {
const layer = this.layers.get(layerName);
if (!layer) {
const layerNames = Array.from(this.layers.keys());
throw new Error(`Unknown layer name "${layerName}". Layer names are: ${layerNames.join('; ')}.`);
}

if (layer.color.equals(color)) {
return; // nothing to do
}

layer.color.set(color);
}

public update(renderer: THREE.WebGLRenderer): void {
const previousState = {
renderTarget: renderer.getRenderTarget(),
clearColor: renderer.getClearColor(new THREE.Color()),
clearAlpha: renderer.getClearAlpha(),
autoClear: renderer.autoClear,
autoClearColor: renderer.autoClearColor,
};

renderer.setRenderTarget(this.renderTarget);
renderer.setClearColor(0x000000, 0);
renderer.autoClear = false;
renderer.autoClearColor = false;
renderer.clear(true);

this.applyLayer.uniforms.layer.value = this.baseTexture;
this.applyLayer.uniforms.color.value = new THREE.Color(0xffffff);
this.applyLayer.uniforms.flipY.value = Number(this.baseTexture.flipY);
this.applyLayer.shader.uniformsNeedUpdate = true;
renderer.render(this.fullscreenQuad, this.fakeCamera);

for (const layer of this.layers.values()) {
this.applyLayer.uniforms.layer.value = layer.texture;
this.applyLayer.uniforms.color.value = layer.color;
this.applyLayer.uniforms.flipY.value = Number(layer.texture.flipY);
this.applyLayer.shader.uniformsNeedUpdate = true;
renderer.render(this.fullscreenQuad, this.fakeCamera);
}

renderer.setRenderTarget(previousState.renderTarget);
renderer.setClearColor(previousState.clearColor);
renderer.setClearAlpha(previousState.clearAlpha);
renderer.autoClear = previousState.autoClear;
renderer.autoClearColor = previousState.autoClearColor;
}
}

export { CustomizableTexture };
12 changes: 12 additions & 0 deletions src/lib/helpers/fullscreen-quad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as THREE from '../libs/three-usage';

function createFullscreenQuad(attributeName: string): THREE.Mesh {
const fullscreenQuadGeometry = new THREE.BufferGeometry();
fullscreenQuadGeometry.setAttribute(attributeName, new THREE.Float32BufferAttribute([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1], 2));
fullscreenQuadGeometry.setDrawRange(0, 6);
const fullscreenQuad = new THREE.Mesh(fullscreenQuadGeometry);
fullscreenQuad.frustumCulled = false;
return fullscreenQuad;
}

export { createFullscreenQuad };
2 changes: 2 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ export { type Spritesheet } from './effects/spritesheet';
export { Rain } from './effects/weather/rain';
export { Snow } from './effects/weather/snow';
export { GpuInstancedBillboard } from './effects/weather/weather-particles-base';

export { CustomizableTexture } from './helpers/customizable-texture';
5 changes: 5 additions & 0 deletions src/lib/libs/three-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
Box3,
BufferGeometry,
Color,
CustomBlending,
DataTexture,
Float32BufferAttribute,
Group,
Expand All @@ -18,6 +19,8 @@ export {
MeshPhongMaterial,
NoBlending,
NormalBlending,
OneFactor,
OneMinusSrcAlphaFactor,
PerspectiveCamera,
PlaneGeometry,
RawShaderMaterial,
Expand All @@ -26,6 +29,7 @@ export {
RGBAFormat,
ShaderMaterial,
Sphere,
SrcAlphaFactor,
Texture,
TextureLoader,
Uint32BufferAttribute,
Expand All @@ -34,6 +38,7 @@ export {
Vector3,
Vector4,
WebGLRenderTarget,
ZeroFactor,
type Frustum,
type IUniform,
type Material,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/terrain/voxelmap/i-voxelmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface ILocalMapData {
* Each element in the array represent a coordinate in the map and stores the data of the voxel at these coordinates.
* Each element should be encoded as follows:
* - bit 0: 0 if the voxel is empty, 1 otherwise
* - bit 1: 1 if the voxel should be displayed as cheesserboard, 0 otherwise
* - bit 1: 1 if the voxel should be displayed as checkerboard, 0 otherwise
* - bits 2-13: ID of the material
* Use the helper "voxelmapDataPacking" to do this encoding and be future-proof.
*
Expand Down
2 changes: 2 additions & 0 deletions src/test/libs/three-usage-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ export {
} from 'three';
export { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
export { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
export { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
export { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
export * from '../../lib/libs/three-usage';
export { Stats };
41 changes: 27 additions & 14 deletions src/test/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,40 @@ import { VoxelMap } from './map/voxel-map';
import { type TestBase } from './test-base';
import { TestTerrain } from './test-terrain';
import { TestTerrainAutonomous } from './test-terrain-autonomous';
import { TestTextureCustomization } from './test-texture-customization';
import { TestWeather } from './test-weather';

setVerbosity(ELogLevel.WARN);

const mapScaleXZ = 800;
const mapScaleY = 200;
const mapSeed = 'fixed_seed';
const includeTreesInLod = false;
function createVoxelMap(): VoxelMap {
const mapScaleXZ = 800;
const mapScaleY = 200;
const mapSeed = 'fixed_seed';
const includeTreesInLod = false;

const voxelMap = new VoxelMap(mapScaleXZ, mapScaleY, mapSeed, includeTreesInLod);
return new VoxelMap(mapScaleXZ, mapScaleY, mapSeed, includeTreesInLod);
}

enum ETest {
TERRAIN,
TERRAIN_OLD,
WEATHER,
TEXTURE_CUSTOMIZATION,
}

const test = ETest.TEXTURE_CUSTOMIZATION as ETest;

let testScene: TestBase;
const testTerrain = false;
if (testTerrain) {
const testNewTerrain = true;
if (testNewTerrain) {
testScene = new TestTerrain(voxelMap);
} else {
testScene = new TestTerrainAutonomous(voxelMap);
}
} else {
if (test === ETest.TERRAIN) {
testScene = new TestTerrain(createVoxelMap());
} else if (test === ETest.TERRAIN_OLD) {
testScene = new TestTerrainAutonomous(createVoxelMap());
} else if (test === ETest.WEATHER) {
testScene = new TestWeather();
} else if (test === ETest.TEXTURE_CUSTOMIZATION) {
testScene = new TestTextureCustomization();
} else {
throw new Error(`Unknown test "${test}".`);
}

testScene.start();
87 changes: 87 additions & 0 deletions src/test/test-texture-customization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { GUI } from 'lil-gui';
import * as THREE from 'three-usage-test';

import { CustomizableTexture } from '../lib';

import { TestBase } from './test-base';

class TestTextureCustomization extends TestBase {
private readonly gui: GUI;

private readonly parameters = {
color1: 0xff0000,
color2: 0x00ff00,
};

private customizableTexture: CustomizableTexture | null = null;

public constructor() {
super();

this.camera.position.set(2, 2, 4);
this.cameraControl.target.set(0, this.camera.position.y - 1.5, 0);

const gridHelper = new THREE.GridHelper(1000, 100);
gridHelper.position.setY(-0.01);
this.scene.add(gridHelper);

const ambientLight = new THREE.AmbientLight(0xffffff);
this.scene.add(ambientLight);

const enforceColors = () => {
this.enforceColors();
};
this.gui = new GUI();
this.gui.addColor(this.parameters, 'color1').onChange(enforceColors);
this.gui.addColor(this.parameters, 'color2').onChange(enforceColors);
enforceColors();

const gltfLoader = new THREE.GLTFLoader();
const dracoLoader = new THREE.DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
dracoLoader.setDecoderConfig({ type: 'js' });
gltfLoader.setDRACOLoader(dracoLoader);

Promise.all([
gltfLoader.loadAsync('/resources/character/iop_male.glb'),
new THREE.TextureLoader().loadAsync('/resources/character/color_01.png'),
new THREE.TextureLoader().loadAsync('/resources/character/color_02.png'),
]).then(([gltf, color1Texture, color2Texture]) => {
this.scene.add(gltf.scene);

gltf.scene.traverse(child => {
if ((child as any).isMesh) {
const childMesh = child as THREE.Mesh;
const childMaterial = childMesh.material as THREE.MeshPhongMaterial;

const childTexture = childMaterial.map;
if (!childTexture) {
throw new Error('No base texture');
}

this.customizableTexture = new CustomizableTexture({
baseTexture: childTexture,
additionalTextures: new Map<string, THREE.Texture>([
['color1', color1Texture],
['color2', color2Texture],
]),
});
this.enforceColors();
childMaterial.map = this.customizableTexture.texture;
}
});
});
}

protected override update(): void {}

private enforceColors(): void {
if (this.customizableTexture) {
this.customizableTexture.setLayerColor('color1', new THREE.Color(this.parameters.color1));
this.customizableTexture.setLayerColor('color2', new THREE.Color(this.parameters.color2));
this.customizableTexture.update(this.renderer);
}
}
}

export { TestTextureCustomization };
Binary file added test/resources/character/char01male.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/resources/character/color_01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/resources/character/color_02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/resources/character/iop_male.glb
Binary file not shown.
Loading