diff --git a/assets/eyes/eye-base.png b/assets/eyes/eye-base.png new file mode 100644 index 0000000..35c4e05 Binary files /dev/null and b/assets/eyes/eye-base.png differ diff --git a/assets/eyes/eye-mask.png b/assets/eyes/eye-mask.png new file mode 100644 index 0000000..b6c566a Binary files /dev/null and b/assets/eyes/eye-mask.png differ diff --git a/src/client/game.ts b/src/client/game.ts index a7405af..8124db5 100644 --- a/src/client/game.ts +++ b/src/client/game.ts @@ -7,6 +7,7 @@ import { ThirdPersonCamera } from './object/camera'; import { Chat } from './object/chat'; import { Joystick } from './object/joystick'; import { CubeMap } from './object/other/cubemap'; +import { PonyEyes } from './object/other/eyes'; import { VideoPlayer } from './object/other/video-player'; import { Player } from './object/player'; import { PlayerEntity } from './object/player-entity'; @@ -40,6 +41,7 @@ export class Game { const cube = await CubeMap.load('/assets/skybox/default'); await modelLoaderInstance.loadPonyModel(); + await PonyEyes.getInstance().initialize(); await this.world.initialize(); this.renderer.initialize(); @@ -57,9 +59,11 @@ export class Game { (item) => item, ); - const colorGet = localStorage.getItem('color'); - if (colorGet) { - this.character.color = colorGet; + const character = localStorage.getItem('character') + ? JSON.parse(localStorage.getItem('character')) + : {}; + if (character) { + this.character = character; } // end of @@ -254,9 +258,28 @@ export class Game { return; } + this.character = { ...this.character, color }; this.player.setColor(color); - this.socket.emit('character', { color }); - localStorage.setItem('color', color); + this.socket.emit('character', this.character); + localStorage.setItem('character', JSON.stringify(this.character)); + } + + if (message.startsWith('!eyecolor')) { + const [cmd, eyeColor] = message.split(' '); + try { + const colorr = new Color(eyeColor); + if (!colorr) { + throw 'invalid'; + } + } catch (e: any) { + this.chat.addMessage('Invalid color.'); + return; + } + + this.character = { ...this.character, eyeColor }; + this.player.setEyeColor(eyeColor); + this.socket.emit('character', this.character); + localStorage.setItem('character', JSON.stringify(this.character)); } if (message.startsWith('!party')) { diff --git a/src/client/object/other/eyes.ts b/src/client/object/other/eyes.ts new file mode 100644 index 0000000..18780d4 --- /dev/null +++ b/src/client/object/other/eyes.ts @@ -0,0 +1,173 @@ +import { Color, ShaderMaterial, UniformsLib, UniformsUtils } from 'three'; +import { BaseTexture } from './texture'; + +export const vertex = /* glsl */ ` +varying vec3 vViewPosition; +varying vec2 vUv; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vUv = uv; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include + #include +} +`; + +export const fragment = /* glsl */ ` +uniform vec3 emissive; +uniform vec3 specular; +uniform float shininess; + +uniform sampler2D baseT; +uniform sampler2D maskT; +uniform vec3 colorMask; + +varying vec2 vUv; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + + vec4 baseTex = texture2D(baseT, vUv); + vec4 maskTex = texture2D(maskT, vUv); + vec3 combine = mix(baseTex.rgb, colorMask, maskTex.r); + + vec4 diffuseColor = vec4( combine, 1.0 ); + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + // accumulation + #include + #include + #include + #include + // modulation + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include + #include +} +`; + +let instance: PonyEyes; + +export class PonyEyes { + private base!: BaseTexture; + private mask!: BaseTexture; + public shader!: ShaderMaterial; + + async initialize() { + this.base = await BaseTexture.load('/assets/eyes/eye-base.png'); + this.mask = await BaseTexture.load('/assets/eyes/eye-mask.png'); + + this.base.texture.flipY = false; + this.mask.texture.flipY = false; + this.shader = new ShaderMaterial({ + vertexShader: vertex, + fragmentShader: fragment, + lights: true, + uniforms: UniformsUtils.merge([ + UniformsLib.common, + UniformsLib.specularmap, + UniformsLib.envmap, + UniformsLib.aomap, + UniformsLib.lightmap, + UniformsLib.emissivemap, + UniformsLib.fog, + UniformsLib.lights, + { + baseT: { value: this.base.texture }, + maskT: { value: this.mask.texture }, + colorMask: { value: new Color(0xff0000) }, + shininess: { value: 100 }, + }, + ]), + }); + } + + public getShader(): ShaderMaterial { + return this.shader.clone(); + } + + public setColor( + shader: ShaderMaterial, + color: number | string, + ): ShaderMaterial { + shader.uniforms.colorMask.value = new Color(color); + return shader; + } + + public static getInstance(): PonyEyes { + if (!instance) { + instance = new PonyEyes(); + } + return instance; + } +} diff --git a/src/client/object/other/texture.ts b/src/client/object/other/texture.ts new file mode 100644 index 0000000..ba965d0 --- /dev/null +++ b/src/client/object/other/texture.ts @@ -0,0 +1,18 @@ +import { TextureLoader, Texture } from 'three'; + +const loader = new TextureLoader(); + +export class BaseTexture { + constructor(public source: string, public texture: Texture) {} + + public static async load(src: string): Promise { + return new Promise((resolve, reject) => + loader.load( + src, + (texture) => resolve(new BaseTexture(src, texture)), + undefined, + reject, + ), + ); + } +} diff --git a/src/client/object/pony.ts b/src/client/object/pony.ts index 7a8b0e9..74d3f5d 100644 --- a/src/client/object/pony.ts +++ b/src/client/object/pony.ts @@ -11,8 +11,10 @@ import { AnimationMixer, Object3D, Vector3, + ShaderMaterial, } from 'three'; import { ClientWorld } from './world/ClientWorld'; +import { PonyEyes } from './other/eyes'; const nameTagBuilder = new CanvasUtils({ fill: false, @@ -43,6 +45,8 @@ export class PonyEntity { .material as MeshStandardMaterial ).clone(); (this.model.children[0].children[1] as Mesh).material = this.material; + (this.model.children[0].children[2] as Mesh).material = + PonyEyes.getInstance().getShader(); this.mixer = new AnimationMixer(this.model); this.idleAction = this.mixer.clipAction(modelLoaderInstance.animations[0]); this.walkAction = this.mixer.clipAction(modelLoaderInstance.animations[2]); @@ -62,10 +66,11 @@ export class PonyEntity { ); // Put pony on the terrain - const terrainFloorHeight = this.heightSource?.getInterpolatedHeight( - this.container.position.x, - this.container.position.z, - ) || 0; + const terrainFloorHeight = + this.heightSource?.getInterpolatedHeight( + this.container.position.x, + this.container.position.z, + ) || 0; this.container.position.y = terrainFloorHeight; this.mixer.update(dt); @@ -103,10 +108,21 @@ export class PonyEntity { this.material.color = new Color(color); } + public setEyeColor(color: number | string) { + PonyEyes.getInstance().setColor( + (this.model.children[0].children[2] as Mesh).material as ShaderMaterial, + color, + ); + } + public setCharacter(packet: CharacterPacket) { if (packet.color) { this.setColor(packet.color); } + + if (packet.eyeColor) { + this.setEyeColor(packet.eyeColor); + } } public setHeightSource(source: ClientWorld) { diff --git a/src/common/types/packet.ts b/src/common/types/packet.ts index 1f5e52b..4fcf8ef 100644 --- a/src/common/types/packet.ts +++ b/src/common/types/packet.ts @@ -19,6 +19,7 @@ export interface FullStatePacket { export interface CharacterPacket { color?: number | string; + eyeColor?: number | string; } export interface CompositePacket extends IcyNetUser, FullStatePacket {}