diff --git a/packages/game/JSExport/GameInterface.cs b/packages/game/JSExport/GameInterface.cs index 7dcfdf12d..5bcfbc42e 100644 --- a/packages/game/JSExport/GameInterface.cs +++ b/packages/game/JSExport/GameInterface.cs @@ -138,7 +138,10 @@ public partial class GameInterface { var path = texturePaths[name]; var texture = provider.LoadPackageObject(path); - return provider.Image(texture); + var isRMAO = name.EndsWith("_RMAO"); + var postProcess = isRMAO ? ImagePostProcess.RMAO : ImagePostProcess.None; + + return provider.Image(texture, postProcess); } public HashSet Files => files; diff --git a/packages/game/Models/BlitzFileProvider.cs b/packages/game/Models/BlitzFileProvider.cs index 778d02bf2..5b867feea 100644 --- a/packages/game/Models/BlitzFileProvider.cs +++ b/packages/game/Models/BlitzFileProvider.cs @@ -17,6 +17,12 @@ using SkiaSharp; namespace BlitzKit.Game.Models; +public enum ImagePostProcess +{ + None, + RMAO, +} + public partial class BlitzFileProvider : DefaultFileProvider { [GeneratedRegex(@"\(GameplayTags=\(\(TagName=""(.+)""\)\)\)")] @@ -87,13 +93,37 @@ public partial class BlitzFileProvider : DefaultFileProvider private readonly SKEncodedImageFormat imageFormat = SKEncodedImageFormat.Webp; private readonly int imageQuality = 80; - public byte[] Image(UTexture2D uTexture) + public byte[] Image(UTexture2D uTexture, ImagePostProcess postProcess = ImagePostProcess.None) { var cTexture = uTexture.Decode(ETexturePlatform.DesktopMobile); ArgumentNullException.ThrowIfNull(cTexture); var bitmap = cTexture.ToSkBitmap(); + + return Image(bitmap, postProcess); + } + + private byte[] Image(SKBitmap bitmap, ImagePostProcess postProcess) + { + if (postProcess != ImagePostProcess.None) + { + for (int y = 0; y < bitmap.Height; y++) + { + for (int x = 0; x < bitmap.Width; x++) + { + var color = bitmap.GetPixel(x, y); + + switch (postProcess) + { + case ImagePostProcess.RMAO: + bitmap.SetPixel(x, y, new SKColor(color.Blue, color.Red, color.Green, color.Alpha)); + break; + } + } + } + } + var data = bitmap.Encode(imageFormat, imageQuality); return data.ToArray(); diff --git a/packages/website-ue/src/components/TankopediaLighting.tsx b/packages/website-ue/src/components/TankopediaLighting.tsx new file mode 100644 index 000000000..5438dbbd0 --- /dev/null +++ b/packages/website-ue/src/components/TankopediaLighting.tsx @@ -0,0 +1,53 @@ +import { times } from "lodash-es"; +import { Quicklime } from "quicklime"; +import { degToRad, lerp } from "three/src/math/MathUtils.js"; + +const ANGLE = Math.PI * 2 ** -2; + +const LIGHTS_COUNT = 5; +const THETA_OFFSET = degToRad(-152); +const LIGHT_DISTANCE = 13; +const LIGHT_HEIGHT_0 = 4; +const LIGHT_HEIGHT_1 = 2; +const INTENSITY_0 = 3.5; +const INTENSITY_1 = 1.5; +const HEMISPHERE_INTENSITY = 2; + +export const transitionEvent = new Quicklime(0); + +export function TankopediaLighting() { + return ( + + + + {times(LIGHTS_COUNT, (index) => { + const x = index / (LIGHTS_COUNT - 1); + const theta = 2 * Math.PI * (index / LIGHTS_COUNT) + THETA_OFFSET; + const position = [ + LIGHT_DISTANCE * Math.sin(theta), + lerp(LIGHT_HEIGHT_0, LIGHT_HEIGHT_1, x), + LIGHT_DISTANCE * Math.cos(theta), + ] as const; + const intensity = lerp(INTENSITY_0, INTENSITY_1, x); + + return ( + + ); + })} + + ); +} diff --git a/packages/website-ue/src/components/TankopediaSandbox/index.tsx b/packages/website-ue/src/components/TankopediaSandbox/index.tsx index 4a346b7d4..583754b9d 100644 --- a/packages/website-ue/src/components/TankopediaSandbox/index.tsx +++ b/packages/website-ue/src/components/TankopediaSandbox/index.tsx @@ -1,14 +1,11 @@ -import { - VisualChanges_TankPart, - type StageParameters, -} from "@protos/blitz_static_tank_upgrade_single_stage"; +import { type StageParameters } from "@protos/blitz_static_tank_upgrade_single_stage"; import { OrbitControls } from "@react-three/drei"; import { Canvas } from "@react-three/fiber"; -import { useProtagonist } from "../../hooks/useProtagonist"; import { useTankChassisModel } from "../../hooks/useTankChassisModel"; import { useTankGunModel } from "../../hooks/useTankGunModel"; import { useTankHullModel } from "../../hooks/useTankHullModel"; import { useTankTurretModel } from "../../hooks/useTankTurretModel"; +import { TankopediaLighting } from "../TankopediaLighting"; import styles from "./index.module.css"; interface TankopediaSandboxProps { @@ -33,17 +30,6 @@ export function TankopediaSandbox({ parameters }: TankopediaSandboxProps) { } function Content({ parameters }: TankopediaSandboxProps) { - const tank = useProtagonist(); - - const selectedTurret = parameters.visual_changes.find( - ({ tank_part }) => tank_part === VisualChanges_TankPart.TANK_PART_TURRET, - )!.name; - const selectedGun = parameters.visual_changes.find( - ({ tank_part }) => tank_part === VisualChanges_TankPart.TANK_PART_GUN, - )!.name; - - const collision = false; - const hull = useTankHullModel(); const chassis = useTankChassisModel(); const turret = useTankTurretModel(parameters); @@ -64,23 +50,11 @@ function Content({ parameters }: TankopediaSandboxProps) { - - - + - - + + diff --git a/packages/website-ue/src/hooks/useTankModel.ts b/packages/website-ue/src/hooks/useTankModel.ts index 6a857e508..472fde1bd 100644 --- a/packages/website-ue/src/hooks/useTankModel.ts +++ b/packages/website-ue/src/hooks/useTankModel.ts @@ -1,33 +1,43 @@ import { useLoader } from "@react-three/fiber"; +import { useEffect } from "react"; import { Mesh, MeshStandardMaterial } from "three"; import { GLTFLoader } from "three-stdlib"; +import { OnObjectUpdate } from "three/src/nodes/utils/EventNode.js"; export function useTankModel(url: string, map: Record) { const gltf = useLoader(GLTFLoader, url); const [root] = gltf.scene.children; - root.traverse((child) => { - if ( - child instanceof Mesh && - child.material instanceof MeshStandardMaterial - ) { - // TODO: remove this hack and fix the gltf - child.material.vertexColors = false; + useEffect(() => { + for (const material of Object.values(gltf.materials)) { + if (material instanceof MeshStandardMaterial) { + // TODO: remove this hack and fix the gltf + material.vertexColors = false; + material.needsUpdate = true; + } } - child.castShadow = true; - child.receiveShadow = true; - }); + console.log(gltf.nodes); - for (const child of root.children) { - if (child.name in map) { - child.visible = map[child.name]; - continue; + for (const node of Object.values(gltf.nodes)) { + if (node instanceof Mesh) { + node.castShadow = true; + node.receiveShadow = true; + } } + }, [url]); - child.visible = false; - console.warn(`Root child ${child.name} isn't recognized`); - } + useEffect(() => { + for (const child of root.children) { + if (child.name in map) { + child.visible = map[child.name]; + continue; + } + + child.visible = false; + console.warn(`Root child ${child.name} isn't recognized`); + } + }, [url, map]); return root; }