mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-11 14:34:51 +00:00
migrate to react-three-fiber
This commit is contained in:
parent
c20ca94953
commit
76e9f68e63
18 changed files with 1367 additions and 752 deletions
7
src/arrayUtils.ts
Normal file
7
src/arrayUtils.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export function uint16ToFloat32(src: Uint16Array) {
|
||||
const out = new Float32Array(src.length);
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
out[i] = src[i] / 65535;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
69
src/loaders.ts
Normal file
69
src/loaders.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { getActualResourcePath, getSource } from "./manifest";
|
||||
import { parseMissionScript } from "./mission";
|
||||
import { parseTerrainBuffer } from "./terrain";
|
||||
|
||||
export const BASE_URL = "/t2-mapper";
|
||||
export const RESOURCE_ROOT_URL = `${BASE_URL}/base/`;
|
||||
|
||||
export function getUrlForPath(resourcePath: string, fallbackUrl?: string) {
|
||||
resourcePath = getActualResourcePath(resourcePath);
|
||||
let sourcePath: string;
|
||||
try {
|
||||
sourcePath = getSource(resourcePath);
|
||||
} catch (err) {
|
||||
if (fallbackUrl) {
|
||||
return fallbackUrl;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (!sourcePath) {
|
||||
return `${RESOURCE_ROOT_URL}${resourcePath}`;
|
||||
} else {
|
||||
return `${RESOURCE_ROOT_URL}@vl2/${sourcePath}/${resourcePath}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function interiorToUrl(name: string) {
|
||||
const difUrl = getUrlForPath(`interiors/${name}`);
|
||||
return difUrl.replace(/\.dif$/i, ".gltf");
|
||||
}
|
||||
|
||||
export function terrainTextureToUrl(name: string) {
|
||||
name = name.replace(/^terrain\./, "");
|
||||
return getUrlForPath(`textures/terrain/${name}.png`, `${BASE_URL}/black.png`);
|
||||
}
|
||||
|
||||
export function interiorTextureToUrl(name: string) {
|
||||
name = name.replace(/\.\d+$/, "");
|
||||
return getUrlForPath(`textures/${name}.png`);
|
||||
}
|
||||
|
||||
export function textureToUrl(name: string) {
|
||||
try {
|
||||
return getUrlForPath(`textures/${name}.png`);
|
||||
} catch (err) {
|
||||
return `${BASE_URL}/black.png`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadDetailMapList(name: string) {
|
||||
const url = getUrlForPath(`textures/${name}`);
|
||||
const res = await fetch(url);
|
||||
const text = await res.text();
|
||||
return text
|
||||
.split(/(?:\r\n|\n|\r)/)
|
||||
.map((line) => `textures/${line.trim().replace(/\.png$/i, "")}.png`);
|
||||
}
|
||||
|
||||
export async function loadMission(name: string) {
|
||||
const res = await fetch(getUrlForPath(`missions/${name}.mis`));
|
||||
const missionScript = await res.text();
|
||||
return parseMissionScript(missionScript);
|
||||
}
|
||||
|
||||
export async function loadTerrain(fileName: string) {
|
||||
const res = await fetch(getUrlForPath(`terrains/${fileName}`));
|
||||
const terrainBuffer = await res.arrayBuffer();
|
||||
return parseTerrainBuffer(terrainBuffer);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import parser from "@/generated/mission.cjs";
|
||||
import { Quaternion, Vector3 } from "three";
|
||||
|
||||
const definitionComment = /^ (DisplayName|MissionTypes) = (.+)$/;
|
||||
const sectionBeginComment = /^--- ([A-Z ]+) BEGIN ---$/;
|
||||
|
|
@ -175,7 +176,8 @@ export function parseMissionScript(script) {
|
|||
};
|
||||
}
|
||||
|
||||
type Mission = ReturnType<typeof parseMissionScript>;
|
||||
export type Mission = ReturnType<typeof parseMissionScript>;
|
||||
export type ConsoleObject = Mission["objects"][number];
|
||||
|
||||
export function* iterObjects(objectList) {
|
||||
for (const obj of objectList) {
|
||||
|
|
@ -186,18 +188,62 @@ export function* iterObjects(objectList) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getTerrainFile(mission: Mission) {
|
||||
let terrainBlock;
|
||||
export function getTerrainBlock(mission: Mission): ConsoleObject {
|
||||
for (const obj of iterObjects(mission.objects)) {
|
||||
if (obj.className === "TerrainBlock") {
|
||||
terrainBlock = obj;
|
||||
break;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
if (!terrainBlock) {
|
||||
throw new Error("Error!");
|
||||
}
|
||||
throw new Error("No TerrainBlock found!");
|
||||
}
|
||||
|
||||
export function getTerrainFile(mission: Mission) {
|
||||
const terrainBlock = getTerrainBlock(mission);
|
||||
return terrainBlock.properties.find(
|
||||
(prop) => prop.target.name === "terrainFile"
|
||||
).value;
|
||||
}
|
||||
|
||||
export function getProperty(obj: ConsoleObject, name: string) {
|
||||
const property = obj.properties.find((p) => p.target.name === name);
|
||||
// console.log({ name, property });
|
||||
return property;
|
||||
}
|
||||
|
||||
export function getPosition(obj: ConsoleObject): [number, number, number] {
|
||||
const position = getProperty(obj, "position")?.value ?? "0 0 0";
|
||||
const [x, z, y] = position.split(" ").map((s) => parseFloat(s));
|
||||
return [x, y, z];
|
||||
}
|
||||
|
||||
export function getScale(obj: ConsoleObject): [number, number, number] {
|
||||
const scale = getProperty(obj, "scale")?.value ?? "1 1 1";
|
||||
const [scaleX, scaleZ, scaleY] = scale.split(" ").map((s) => parseFloat(s));
|
||||
return [scaleX, scaleY, scaleZ];
|
||||
}
|
||||
|
||||
export function getRotation(obj: ConsoleObject, isInterior = false) {
|
||||
const rotation = getProperty(obj, "rotation")?.value ?? "1 0 0 0";
|
||||
const [ax, az, ay, angle] = rotation.split(" ").map((s) => parseFloat(s));
|
||||
|
||||
if (isInterior) {
|
||||
// For interiors: Apply coordinate system transformation
|
||||
// 1. Convert rotation axis from source coords (ax, az, ay) to Three.js coords
|
||||
// 2. Apply -90 Y rotation to align coordinate systems
|
||||
const sourceRotation = new Quaternion().setFromAxisAngle(
|
||||
new Vector3(az, ay, ax),
|
||||
-angle * (Math.PI / 180)
|
||||
);
|
||||
const coordSystemFix = new Quaternion().setFromAxisAngle(
|
||||
new Vector3(0, 1, 0),
|
||||
Math.PI / 2
|
||||
);
|
||||
return coordSystemFix.multiply(sourceRotation);
|
||||
} else {
|
||||
// For other objects (terrain, etc)
|
||||
return new Quaternion().setFromAxisAngle(
|
||||
new Vector3(ax, ay, -az),
|
||||
angle * (Math.PI / 180)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
168
src/textureUtils.ts
Normal file
168
src/textureUtils.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import {
|
||||
DataTexture,
|
||||
LinearFilter,
|
||||
LinearMipmapLinearFilter,
|
||||
NoColorSpace,
|
||||
RedFormat,
|
||||
RepeatWrapping,
|
||||
SRGBColorSpace,
|
||||
UnsignedByteType,
|
||||
} from "three";
|
||||
|
||||
export function setupColor(tex, repeat = [1, 1]) {
|
||||
tex.wrapS = tex.wrapT = RepeatWrapping;
|
||||
tex.colorSpace = SRGBColorSpace;
|
||||
tex.repeat.set(...repeat);
|
||||
tex.anisotropy = 16;
|
||||
tex.generateMipmaps = true;
|
||||
tex.minFilter = LinearMipmapLinearFilter;
|
||||
tex.magFilter = LinearFilter;
|
||||
|
||||
tex.needsUpdate = true;
|
||||
|
||||
return tex;
|
||||
}
|
||||
|
||||
export function setupMask(data) {
|
||||
const tex = new DataTexture(
|
||||
data,
|
||||
256,
|
||||
256,
|
||||
RedFormat, // 1 channel
|
||||
UnsignedByteType // 8-bit
|
||||
);
|
||||
|
||||
// Masks should stay linear
|
||||
tex.colorSpace = NoColorSpace;
|
||||
|
||||
// Set tiling / sampling. For NPOT sizes, disable mips or use power-of-two.
|
||||
tex.wrapS = tex.wrapT = RepeatWrapping;
|
||||
tex.generateMipmaps = false; // if width/height are not powers of two
|
||||
tex.minFilter = LinearFilter; // avoid mips if generateMipmaps=false
|
||||
tex.magFilter = LinearFilter;
|
||||
|
||||
tex.needsUpdate = true;
|
||||
|
||||
return tex;
|
||||
}
|
||||
|
||||
export function updateTerrainTextureShader({
|
||||
shader,
|
||||
baseTextures,
|
||||
alphaTextures,
|
||||
visibilityMask,
|
||||
}) {
|
||||
const layerCount = baseTextures.length;
|
||||
|
||||
baseTextures.forEach((tex, i) => {
|
||||
shader.uniforms[`albedo${i}`] = { value: tex };
|
||||
});
|
||||
|
||||
alphaTextures.forEach((tex, i) => {
|
||||
if (i > 0) {
|
||||
shader.uniforms[`mask${i}`] = { value: tex };
|
||||
}
|
||||
});
|
||||
|
||||
// Add visibility mask uniform if we have empty squares
|
||||
if (visibilityMask) {
|
||||
shader.uniforms.visibilityMask = { value: visibilityMask };
|
||||
}
|
||||
|
||||
// Add per-texture tiling uniforms
|
||||
baseTextures.forEach((tex, i) => {
|
||||
shader.uniforms[`tiling${i}`] = {
|
||||
value: Math.min(512, { 0: 16, 1: 16, 2: 32, 3: 32, 4: 32, 5: 32 }[i]),
|
||||
};
|
||||
});
|
||||
|
||||
// Declare our uniforms at the top of the fragment shader
|
||||
shader.fragmentShader =
|
||||
`
|
||||
uniform sampler2D albedo0;
|
||||
uniform sampler2D albedo1;
|
||||
uniform sampler2D albedo2;
|
||||
uniform sampler2D albedo3;
|
||||
uniform sampler2D albedo4;
|
||||
uniform sampler2D albedo5;
|
||||
uniform sampler2D mask1;
|
||||
uniform sampler2D mask2;
|
||||
uniform sampler2D mask3;
|
||||
uniform sampler2D mask4;
|
||||
uniform sampler2D mask5;
|
||||
uniform float tiling0;
|
||||
uniform float tiling1;
|
||||
uniform float tiling2;
|
||||
uniform float tiling3;
|
||||
uniform float tiling4;
|
||||
uniform float tiling5;
|
||||
${visibilityMask ? "uniform sampler2D visibilityMask;" : ""}
|
||||
` + shader.fragmentShader;
|
||||
|
||||
if (visibilityMask) {
|
||||
const clippingPlaceholder = "#include <clipping_planes_fragment>";
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
clippingPlaceholder,
|
||||
`${clippingPlaceholder}
|
||||
// Early discard for invisible areas (before fog/lighting)
|
||||
float visibility = texture2D(visibilityMask, vMapUv).r;
|
||||
if (visibility < 0.5) {
|
||||
discard;
|
||||
}
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
// Replace the default map sampling block with our layered blend.
|
||||
// We rely on vMapUv provided by USE_MAP.
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
"#include <map_fragment>",
|
||||
`
|
||||
// Sample base albedo layers (sRGB textures auto-decoded to linear)
|
||||
vec2 baseUv = vMapUv;
|
||||
vec3 c0 = texture2D(albedo0, baseUv * vec2(tiling0)).rgb;
|
||||
${
|
||||
layerCount > 1
|
||||
? `vec3 c1 = texture2D(albedo1, baseUv * vec2(tiling1)).rgb;`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
layerCount > 2
|
||||
? `vec3 c2 = texture2D(albedo2, baseUv * vec2(tiling2)).rgb;`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
layerCount > 3
|
||||
? `vec3 c3 = texture2D(albedo3, baseUv * vec2(tiling3)).rgb;`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
layerCount > 4
|
||||
? `vec3 c4 = texture2D(albedo4, baseUv * vec2(tiling4)).rgb;`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
layerCount > 5
|
||||
? `vec3 c5 = texture2D(albedo5, baseUv * vec2(tiling5)).rgb;`
|
||||
: ""
|
||||
}
|
||||
|
||||
// Sample linear masks (use R channel)
|
||||
float a1 = texture2D(mask1, baseUv).r;
|
||||
${layerCount > 1 ? `float a2 = texture2D(mask2, baseUv).r;` : ""}
|
||||
${layerCount > 2 ? `float a3 = texture2D(mask3, baseUv).r;` : ""}
|
||||
${layerCount > 3 ? `float a4 = texture2D(mask4, baseUv).r;` : ""}
|
||||
${layerCount > 4 ? `float a5 = texture2D(mask5, baseUv).r;` : ""}
|
||||
|
||||
// Bottom-up compositing: each mask tells how much the higher layer replaces lower
|
||||
${layerCount > 1 ? `vec3 blended = mix(c0, c1, clamp(a1, 0.0, 1.0));` : ""}
|
||||
${layerCount > 2 ? `blended = mix(blended, c2, clamp(a2, 0.0, 1.0));` : ""}
|
||||
${layerCount > 3 ? `blended = mix(blended, c3, clamp(a3, 0.0, 1.0));` : ""}
|
||||
${layerCount > 4 ? `blended = mix(blended, c4, clamp(a4, 0.0, 1.0));` : ""}
|
||||
${layerCount > 5 ? `blended = mix(blended, c5, clamp(a5, 0.0, 1.0));` : ""}
|
||||
|
||||
// Assign to diffuseColor before lighting
|
||||
diffuseColor.rgb = ${layerCount > 1 ? "blended" : "c0"};
|
||||
`
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue