mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-25 03:29:51 +01:00
Compare commits
5
Commits
62b629fe3a
...
965acadc3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
965acadc3f | ||
|
|
14a2658a83 | ||
|
|
5cbe08fe1c | ||
|
|
05dd4dbd98 | ||
|
|
a348e005f7 |
No files matched your search
@@ -37,7 +37,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
chunk: [pages, api, media, models]
|
||||
chunk: [pages, api, media, models, textures]
|
||||
|
||||
steps:
|
||||
- name: Free disk space
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using BlitzKit.Game.Models;
|
||||
using CUE4Parse_Conversion;
|
||||
using CUE4Parse_Conversion.Materials;
|
||||
using CUE4Parse_Conversion.Meshes;
|
||||
using CUE4Parse_Conversion.Textures;
|
||||
using CUE4Parse_Conversion.UEFormat.Enums;
|
||||
using CUE4Parse.UE4.Assets.Exports;
|
||||
using CUE4Parse.UE4.Assets.Exports.Engine;
|
||||
using CUE4Parse.UE4.Assets.Exports.Material;
|
||||
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
|
||||
using CUE4Parse.UE4.Assets.Exports.Texture;
|
||||
using CUE4Parse.UE4.Assets.Objects;
|
||||
@@ -31,6 +33,87 @@ public partial class GameInterface
|
||||
files = [.. provider.Files.Keys];
|
||||
}
|
||||
|
||||
readonly Dictionary<string, string> texturePaths = new();
|
||||
|
||||
public void DiscoverTextures(string[] tankTags)
|
||||
{
|
||||
Console.WriteLine("Discovering textures...");
|
||||
|
||||
var options = new ExporterOptions() { TextureFormat = ETextureFormat.Jpeg };
|
||||
|
||||
foreach (var tag in tankTags)
|
||||
{
|
||||
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
|
||||
|
||||
foreach (var group in tankPartPrefixes.Values)
|
||||
{
|
||||
var dataTable = pda.Get<UDataTable>(group);
|
||||
|
||||
foreach (var row in dataTable.RowMap.Values)
|
||||
{
|
||||
var visualData = row.Get<UObject>("VisualData");
|
||||
var meshSettings = visualData.Get<FStructFallback>("MeshSettings");
|
||||
|
||||
foreach (var property in meshSettings.Properties)
|
||||
{
|
||||
if (meshSettings.TryGet<UStaticMesh>(property.Name.Text, out var mesh))
|
||||
{
|
||||
mesh.TryConvert(out var convertedMesh);
|
||||
|
||||
var lod0 = convertedMesh.LODs.Find(lod => !lod.SkipLod);
|
||||
|
||||
if (lod0 == null || lod0.Sections == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var section in lod0.Sections.Value)
|
||||
{
|
||||
if (section.Material == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var materialInterface = section.Material.Load<UMaterialInterface>();
|
||||
|
||||
if (materialInterface == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var materialData = new MaterialData() { Parameters = new() };
|
||||
|
||||
materialInterface.GetParams(materialData.Parameters, options.MaterialFormat);
|
||||
|
||||
foreach (var parameterTexture in materialData.Parameters.Textures)
|
||||
{
|
||||
if (MonoGltf.knownChannels.TryGetValue(parameterTexture.Key, out var channel))
|
||||
{
|
||||
var path = parameterTexture.Value.GetPathName();
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
texturePaths[name] = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"Discovered {texturePaths.Count} textures");
|
||||
}
|
||||
|
||||
public List<string> Textures => [.. texturePaths.Keys];
|
||||
|
||||
public byte[] Texture(string name)
|
||||
{
|
||||
var path = texturePaths[name];
|
||||
var texture = provider.LoadPackageObject<UTexture2D>(path);
|
||||
return provider.Image(texture);
|
||||
}
|
||||
|
||||
public HashSet<string> Files => files;
|
||||
|
||||
public byte[] TankBigIcon(string tag)
|
||||
@@ -141,16 +224,32 @@ public partial class GameInterface
|
||||
|
||||
var visualData = row.Get<UObject>("VisualData");
|
||||
var meshSettings = visualData.Get<FStructFallback>("MeshSettings");
|
||||
// var collisionMesh = meshSettings.Get<UStaticMesh>("CollisionMesh");
|
||||
var mesh = meshSettings.Get<UStaticMesh>("Mesh");
|
||||
var meshes = new List<UStaticMesh>();
|
||||
|
||||
var gltf = new MonoGltf(mesh);
|
||||
foreach (var property in meshSettings.Properties)
|
||||
{
|
||||
var name = property.Name.Text;
|
||||
|
||||
if (name == "CollisionMesh")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (meshSettings.TryGet<UStaticMesh>(name, out var mesh))
|
||||
{
|
||||
meshes.Add(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
var gltf = new MonoGltf(meshes);
|
||||
|
||||
return gltf.Write($"{tag}/{part}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new Exception($"Failed to export tank part: {part}", e);
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -32,88 +32,100 @@ public class MonoGltf
|
||||
{
|
||||
readonly SceneBuilder scene = new();
|
||||
|
||||
readonly Dictionary<string, KnownChannel> knownChannels = new()
|
||||
public static readonly Dictionary<string, KnownChannel> knownChannels = new()
|
||||
{
|
||||
{ "BaseColor", KnownChannel.BaseColor },
|
||||
{ "Normal", KnownChannel.Normal },
|
||||
{ "RMAO", KnownChannel.MetallicRoughness },
|
||||
// { "CDE", ?? },
|
||||
};
|
||||
readonly string stubTexture = "../../stub/stupid.png";
|
||||
readonly byte[] stubBytes = File.ReadAllBytes("../game/stub/small.png");
|
||||
|
||||
public MonoGltf(UStaticMesh staticMesh)
|
||||
public MonoGltf(List<UStaticMesh> meshes)
|
||||
{
|
||||
var options = new ExporterOptions() { TextureFormat = ETextureFormat.Jpeg };
|
||||
|
||||
staticMesh.TryConvert(out var convertedMesh);
|
||||
var lod =
|
||||
convertedMesh.LODs.Find(lod => !lod.SkipLod) ?? throw new Exception("Failed to find lod0");
|
||||
|
||||
var emptyMaterial = new MaterialBuilder("empty_material");
|
||||
var materialMap = new Dictionary<string, MaterialBuilder>();
|
||||
|
||||
int sectionIndex = 0;
|
||||
foreach (var section in lod.Sections.Value)
|
||||
foreach (var mesh in meshes)
|
||||
{
|
||||
var meshBuilder = new ConfiguredMeshBuilder();
|
||||
MaterialBuilder materialBuilder;
|
||||
mesh.TryConvert(out var converted);
|
||||
var lod0 =
|
||||
converted.LODs.Find(lod => !lod.SkipLod) ?? throw new Exception("Failed to find lod0");
|
||||
|
||||
if (section.Material == null || section.MaterialName == null)
|
||||
{
|
||||
materialBuilder = emptyMaterial;
|
||||
}
|
||||
else if (materialMap.TryGetValue(section.MaterialName, out MaterialBuilder? value))
|
||||
{
|
||||
materialBuilder = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
var materialInterface = section.Material.Load<UMaterialInterface>()!;
|
||||
var materialData = new MaterialData() { Parameters = new() };
|
||||
var emptyMaterial = new MaterialBuilder("empty_material");
|
||||
var materialMap = new Dictionary<string, MaterialBuilder>();
|
||||
|
||||
materialInterface.GetParams(materialData.Parameters, options.MaterialFormat);
|
||||
materialBuilder = new();
|
||||
materialMap.Add(section.MaterialName, materialBuilder);
|
||||
int sectionIndex = 0;
|
||||
foreach (var section in lod0.Sections.Value)
|
||||
{
|
||||
var meshBuilder = new ConfiguredMeshBuilder();
|
||||
MaterialBuilder materialBuilder;
|
||||
|
||||
foreach (var parameterTexture in materialData.Parameters.Textures)
|
||||
if (section.Material == null || section.MaterialName == null)
|
||||
{
|
||||
if (knownChannels.TryGetValue(parameterTexture.Key, out var channel))
|
||||
{
|
||||
var texture = parameterTexture.Value;
|
||||
var name = Path.GetFileNameWithoutExtension(texture.GetPathName());
|
||||
var path = $"../../../textures/{name}.webp";
|
||||
materialBuilder = emptyMaterial;
|
||||
}
|
||||
else if (materialMap.TryGetValue(section.MaterialName, out MaterialBuilder? value))
|
||||
{
|
||||
materialBuilder = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
var materialInterface = section.Material.Load<UMaterialInterface>()!;
|
||||
var materialData = new MaterialData() { Parameters = new() };
|
||||
|
||||
materialBuilder.WithChannelImage(channel, stubTexture);
|
||||
materialBuilder.GetChannel(channel).Texture.PrimaryImage.Name = path;
|
||||
}
|
||||
else
|
||||
materialInterface.GetParams(materialData.Parameters, options.MaterialFormat);
|
||||
materialBuilder = new();
|
||||
materialMap.Add(section.MaterialName, materialBuilder);
|
||||
|
||||
foreach (var parameterTexture in materialData.Parameters.Textures)
|
||||
{
|
||||
Console.WriteLine($"Unsupported texture type: {parameterTexture.Key}");
|
||||
if (knownChannels.TryGetValue(parameterTexture.Key, out var channel))
|
||||
{
|
||||
var texture = parameterTexture.Value;
|
||||
var name = Path.GetFileNameWithoutExtension(texture.GetPathName());
|
||||
var path = $"../../../textures/{name}.webp";
|
||||
|
||||
var lastIndex = stubBytes.Length - 1;
|
||||
stubBytes[lastIndex] = (byte)(++stubBytes[lastIndex] % 255);
|
||||
var uniqueBytes = (byte[])stubBytes.Clone();
|
||||
var stubImage = new MemoryImage(uniqueBytes);
|
||||
|
||||
// Console.WriteLine(
|
||||
// $"created stub image {stubImage} @ {stubImage.SourcePath} w/ byte {uniqueBytes[lastIndex]}"
|
||||
// );
|
||||
|
||||
materialBuilder.WithChannelImage(channel, stubImage);
|
||||
materialBuilder.GetChannel(channel).Texture.PrimaryImage.Name = path;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Console.WriteLine($"Unsupported texture type: {parameterTexture.Key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var primitive = meshBuilder.UsePrimitive(materialBuilder);
|
||||
var primitive = meshBuilder.UsePrimitive(materialBuilder);
|
||||
|
||||
for (int faceIndex = 0; faceIndex < section.NumFaces; faceIndex++)
|
||||
{
|
||||
var wedgeIndex = new int[3];
|
||||
for (var k = 0; k < wedgeIndex.Length; k++)
|
||||
for (int faceIndex = 0; faceIndex < section.NumFaces; faceIndex++)
|
||||
{
|
||||
wedgeIndex[k] = (int)lod.Indices.Value[section.FirstIndex + faceIndex * 3 + k];
|
||||
var wedgeIndex = new int[3];
|
||||
for (var k = 0; k < wedgeIndex.Length; k++)
|
||||
{
|
||||
wedgeIndex[k] = (int)lod0.Indices.Value[section.FirstIndex + faceIndex * 3 + k];
|
||||
}
|
||||
|
||||
var vertex1 = lod0.Verts[wedgeIndex[0]];
|
||||
var vertex2 = lod0.Verts[wedgeIndex[1]];
|
||||
var vertex3 = lod0.Verts[wedgeIndex[2]];
|
||||
|
||||
var (v1, v2, v3) = PrepareTris(vertex1, vertex2, vertex3);
|
||||
var (c1, c2, c3) = PrepareUVsAndTexCoords(lod0, vertex1, vertex2, vertex3, wedgeIndex);
|
||||
|
||||
primitive.AddTriangle((v1, c1), (v2, c2), (v3, c3));
|
||||
}
|
||||
|
||||
var vertex1 = lod.Verts[wedgeIndex[0]];
|
||||
var vertex2 = lod.Verts[wedgeIndex[1]];
|
||||
var vertex3 = lod.Verts[wedgeIndex[2]];
|
||||
|
||||
var (v1, v2, v3) = PrepareTris(vertex1, vertex2, vertex3);
|
||||
var (c1, c2, c3) = PrepareUVsAndTexCoords(lod, vertex1, vertex2, vertex3, wedgeIndex);
|
||||
|
||||
primitive.AddTriangle((v1, c1), (v2, c2), (v3, c3));
|
||||
scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
|
||||
}
|
||||
|
||||
scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,8 +138,11 @@ public class MonoGltf
|
||||
|
||||
var mappedTextures = new Dictionary<MemoryImage, string>();
|
||||
|
||||
// Console.WriteLine($"found {model.LogicalImages.Count} logical images");
|
||||
|
||||
foreach (var image in model.LogicalImages)
|
||||
{
|
||||
// Console.WriteLine($"mapping {image.Content} ({image.Name})");
|
||||
mappedTextures[image.Content] = image.Name!;
|
||||
image.Name = null;
|
||||
}
|
||||
@@ -141,6 +156,8 @@ public class MonoGltf
|
||||
ImageWriting = ResourceWriteMode.SatelliteFile,
|
||||
ImageWriteCallback = (context, assetName, image) =>
|
||||
{
|
||||
// Console.WriteLine($"got {image} -> {mappedTextures[image]}");
|
||||
|
||||
return mappedTextures[image];
|
||||
},
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 95 B |
@@ -1,6 +1,8 @@
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
import { Canvas, useLoader } from "@react-three/fiber";
|
||||
import { Mesh, MeshStandardMaterial } from "three";
|
||||
import { GLTFLoader } from "three-stdlib";
|
||||
import { useProtagonist } from "../../hooks/useProtagonist";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
export function TankopediaSandbox() {
|
||||
@@ -21,27 +23,59 @@ export function TankopediaSandbox() {
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const gltf = useLoader(
|
||||
GLTFLoader,
|
||||
"/models/tanks/TankEntity.A128_Concept_1b/hull.glb",
|
||||
);
|
||||
const tank = useProtagonist();
|
||||
|
||||
gltf.scene.traverse((child) => {
|
||||
child.castShadow = true;
|
||||
});
|
||||
const turret = useLoader(
|
||||
GLTFLoader,
|
||||
`/models/tanks/${tank.id}/turret_01.glb`,
|
||||
);
|
||||
const gun = useLoader(GLTFLoader, `/models/tanks/${tank.id}/gun_01.glb`);
|
||||
const hull = useLoader(GLTFLoader, `/models/tanks/${tank.id}/hull.glb`);
|
||||
const chassis = useLoader(GLTFLoader, `/models/tanks/${tank.id}/chassis.glb`);
|
||||
|
||||
const gltfs = [turret, gun, hull, chassis];
|
||||
|
||||
for (const gltf of gltfs) {
|
||||
gltf.scene.traverse((child) => {
|
||||
if (
|
||||
child instanceof Mesh &&
|
||||
child.material instanceof MeshStandardMaterial
|
||||
) {
|
||||
// TODO: remove this hack and fix the gltf
|
||||
child.material.vertexColors = false;
|
||||
}
|
||||
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<group>
|
||||
<primitive object={gltf.scene} />
|
||||
<primitive object={chassis.scene} />
|
||||
<primitive object={hull.scene} />
|
||||
</group>
|
||||
|
||||
<pointLight castShadow position={[3, 10, 5]} intensity={2 ** 14} />
|
||||
<ambientLight intensity={1} />
|
||||
<group position={[0, 0, 5]}>
|
||||
<primitive object={turret.scene} />
|
||||
</group>
|
||||
|
||||
<mesh receiveShadow position={[0, -1, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<group position={[0, 0, -5]}>
|
||||
<primitive object={gun.scene} />
|
||||
</group>
|
||||
|
||||
<spotLight
|
||||
castShadow
|
||||
position={[3, 10, 5]}
|
||||
intensity={2 ** 10}
|
||||
penumbra={1}
|
||||
/>
|
||||
<ambientLight />
|
||||
|
||||
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[2 ** 8, 2 ** 8]} />
|
||||
<meshStandardMaterial color="black" />
|
||||
<meshStandardMaterial color="gray" />
|
||||
</mesh>
|
||||
|
||||
<OrbitControls />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { assertSecret } from "@blitzkit/core";
|
||||
import type { GameInterface } from "@blitzkit/game";
|
||||
import { clientUnmounted } from "./unmounted";
|
||||
import { api } from "../api/dynamic";
|
||||
import { assertDiscoveryTag } from "./assertDiscoveryTag";
|
||||
|
||||
let _game: GameInterface | null = null;
|
||||
|
||||
@@ -12,6 +14,17 @@ if (!clientUnmounted && import.meta.env.SSR) {
|
||||
assertSecret(import.meta.env.WOTB_USMAP),
|
||||
"../../temp",
|
||||
);
|
||||
|
||||
const tankTags: string[] = [];
|
||||
|
||||
for (const item of api.metadata.group("TankEntity")) {
|
||||
const assetDiscovery = item.BlitzStaticAssetsDiscovery();
|
||||
const tag = assertDiscoveryTag(assetDiscovery);
|
||||
|
||||
tankTags.push(tag);
|
||||
}
|
||||
|
||||
_game.discoverTextures(tankTags);
|
||||
}
|
||||
|
||||
export const game = _game;
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { APIContext } from "astro";
|
||||
import { mixStaticPaths } from "../../astro/mixStaticPaths";
|
||||
import { game } from "../../game/game";
|
||||
import { getStaticPaths as _getStaticPaths } from "./_index";
|
||||
|
||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
|
||||
return game!.textures.map((name) => ({ params: { name } }));
|
||||
});
|
||||
|
||||
export function GET({ params }: APIContext<never, { name: string }>) {
|
||||
const image = game!.texture(params.name);
|
||||
const array = new Uint8Array(image);
|
||||
return new Response(array);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { isChunkEnabled } from "../../astro/isChunkEnabled";
|
||||
|
||||
export const getStaticPaths = (() => {
|
||||
if (!isChunkEnabled("textures")) return [];
|
||||
return [{ params: { textures: "textures" } }];
|
||||
}) satisfies GetStaticPaths;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 886 KiB |
Reference in new issue
Block a user