nexusbert's picture
ready player implementation
9557505
import express from "express";
import { AppDataSource } from "../utils/dataSource";
import { User } from "../entity/User";
import { authenticateToken, AuthRequest } from "../middleware/auth";
import { readyPlayerMeClient } from "../utils/readyPlayerMe";
const router = express.Router();
/**
* @openapi
* /api/avatar/create:
* post:
* summary: Save Ready Player Me avatar ID for user
* tags: [Avatar]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - avatarId
* properties:
* avatarId:
* type: string
* description: Ready Player Me avatar ID
* responses:
* 200:
* description: Avatar ID saved successfully
* 400:
* description: Bad request (missing avatarId)
* 401:
* description: Unauthorized
* 404:
* description: User not found
* 500:
* description: Server error
*/
router.post("/create", authenticateToken, async (req: AuthRequest, res) => {
try {
const userId = req.userId!;
const { avatarId } = req.body;
if (!avatarId) {
return res.status(400).json({
success: false,
error: "avatarId is required",
});
}
const userRepo = AppDataSource.getRepository(User);
const user = await userRepo.findOne({ where: { id: userId } });
if (!user) {
return res.status(404).json({
success: false,
error: "User not found",
});
}
user.readyPlayerMeAvatarId = avatarId;
await userRepo.save(user);
res.json({
success: true,
message: "Avatar ID saved successfully",
avatarId,
});
} catch (error: any) {
console.error("Avatar creation error:", error);
res.status(500).json({
success: false,
error: error.message || "Failed to save avatar ID",
});
}
});
/**
* @openapi
* /api/avatar/glb:
* get:
* summary: Get 3D avatar GLB file URL
* tags: [Avatar]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: quality
* schema:
* type: string
* enum: [low, medium, high, ultra]
* description: Avatar quality preset
* - in: query
* name: lod
* schema:
* type: integer
* enum: [0, 1, 2]
* description: Level of detail (0=full, 1=50%, 2=25%)
* - in: query
* name: textureAtlas
* schema:
* type: string
* description: Texture atlas size or "none"
* - in: query
* name: textureFormat
* schema:
* type: string
* enum: [webp, jpeg, png]
* description: Texture format
* - in: query
* name: useDracoMeshCompression
* schema:
* type: boolean
* description: Enable Draco mesh compression
* responses:
* 200:
* description: Avatar GLB URL retrieved successfully
* 401:
* description: Unauthorized
* 404:
* description: Avatar not found
* 500:
* description: Server error
*/
router.get("/glb", authenticateToken, async (req: AuthRequest, res) => {
try {
const userId = req.userId!;
const { quality, lod, textureAtlas, textureFormat, useDracoMeshCompression } = req.query;
const userRepo = AppDataSource.getRepository(User);
const user = await userRepo.findOne({ where: { id: userId } });
if (!user || !user.readyPlayerMeAvatarId) {
return res.status(404).json({
success: false,
error: "Avatar not found. Please create an avatar first.",
});
}
const avatarUrl = await readyPlayerMeClient.getAvatarGLB(user.readyPlayerMeAvatarId, {
quality: quality as any,
lod: lod ? parseInt(lod as string) : undefined,
textureAtlas: textureAtlas === "none" ? "none" : textureAtlas ? parseInt(textureAtlas as string) : undefined,
textureFormat: textureFormat as any,
useDracoMeshCompression: useDracoMeshCompression === "true",
});
res.json({
success: true,
avatarUrl,
avatarId: user.readyPlayerMeAvatarId,
});
} catch (error: any) {
console.error("Get avatar GLB error:", error);
res.status(500).json({
success: false,
error: error.message || "Failed to get avatar GLB",
});
}
});
/**
* @openapi
* /api/avatar/render:
* get:
* summary: Get 2D avatar render (portrait) URL
* tags: [Avatar]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: size
* schema:
* type: integer
* minimum: 1
* maximum: 1024
* description: Image size in pixels
* - in: query
* name: quality
* schema:
* type: integer
* minimum: 0
* maximum: 100
* description: Image compression quality
* - in: query
* name: camera
* schema:
* type: string
* enum: [portrait, fullbody, fit]
* description: Camera preset
* - in: query
* name: background
* schema:
* type: string
* description: Background color (RGB format)
* - in: query
* name: expression
* schema:
* type: string
* description: Facial expression
* - in: query
* name: pose
* schema:
* type: string
* description: Avatar pose
* responses:
* 200:
* description: Avatar render URL retrieved successfully
* 401:
* description: Unauthorized
* 404:
* description: Avatar not found
* 500:
* description: Server error
*/
router.get("/render", authenticateToken, async (req: AuthRequest, res) => {
try {
const userId = req.userId!;
const { size, quality, camera, background, expression, pose } = req.query;
const userRepo = AppDataSource.getRepository(User);
const user = await userRepo.findOne({ where: { id: userId } });
if (!user || !user.readyPlayerMeAvatarId) {
return res.status(404).json({
success: false,
error: "Avatar not found. Please create an avatar first.",
});
}
const renderUrl = await readyPlayerMeClient.getAvatar2DRender(user.readyPlayerMeAvatarId, {
size: size ? parseInt(size as string) : undefined,
quality: quality ? parseInt(quality as string) : undefined,
camera: camera as any,
background: background as string,
expression: expression as string,
pose: pose as string,
});
res.json({
success: true,
renderUrl,
avatarId: user.readyPlayerMeAvatarId,
});
} catch (error: any) {
console.error("Get avatar render error:", error);
res.status(500).json({
success: false,
error: error.message || "Failed to get avatar render",
});
}
});
/**
* @openapi
* /api/avatar/metadata:
* get:
* summary: Get avatar metadata
* tags: [Avatar]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Avatar metadata retrieved successfully
* 401:
* description: Unauthorized
* 404:
* description: Avatar not found
* 500:
* description: Server error
*/
router.get("/metadata", authenticateToken, async (req: AuthRequest, res) => {
try {
const userId = req.userId!;
const userRepo = AppDataSource.getRepository(User);
const user = await userRepo.findOne({ where: { id: userId } });
if (!user || !user.readyPlayerMeAvatarId) {
return res.status(404).json({
success: false,
error: "Avatar not found. Please create an avatar first.",
});
}
const metadata = await readyPlayerMeClient.getAvatarMetadata(user.readyPlayerMeAvatarId);
if (!metadata) {
return res.status(404).json({
success: false,
error: "Avatar metadata not found",
});
}
res.json({
success: true,
metadata,
});
} catch (error: any) {
console.error("Get avatar metadata error:", error);
res.status(500).json({
success: false,
error: error.message || "Failed to get avatar metadata",
});
}
});
/**
* @openapi
* /api/avatar/try-on:
* post:
* summary: Equip assets to avatar and get GLB URL with outfit
* tags: [Avatar]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - assetIds
* properties:
* assetIds:
* type: array
* items:
* type: string
* description: Array of Ready Player Me asset IDs to equip
* quality:
* type: string
* enum: [low, medium, high, ultra]
* description: Avatar quality preset
* responses:
* 200:
* description: Avatar GLB URL with equipped outfit
* 400:
* description: Bad request
* 401:
* description: Unauthorized
* 404:
* description: Avatar not found
* 500:
* description: Server error
*/
router.post("/try-on", authenticateToken, async (req: AuthRequest, res) => {
try {
const userId = req.userId!;
const { assetIds, quality } = req.body;
if (!assetIds || !Array.isArray(assetIds) || assetIds.length === 0) {
return res.status(400).json({
success: false,
error: "assetIds array is required",
});
}
const userRepo = AppDataSource.getRepository(User);
const user = await userRepo.findOne({ where: { id: userId } });
if (!user || !user.readyPlayerMeAvatarId) {
return res.status(404).json({
success: false,
error: "Avatar not found. Please create an avatar first.",
});
}
// Equip all assets to avatar
const equipPromises = assetIds.map((assetId: string) =>
readyPlayerMeClient.equipAsset(user.readyPlayerMeAvatarId!, assetId)
);
const results = await Promise.all(equipPromises);
const allEquipped = results.every((result) => result === true);
if (!allEquipped) {
console.warn("Some assets failed to equip");
}
// Get avatar GLB URL with equipped outfit
const avatarUrl = await readyPlayerMeClient.getAvatarGLB(user.readyPlayerMeAvatarId, {
quality: quality || "medium",
lod: 1,
textureFormat: "webp",
useDracoMeshCompression: true,
});
res.json({
success: true,
avatarUrl,
avatarId: user.readyPlayerMeAvatarId,
equippedAssets: assetIds,
});
} catch (error: any) {
console.error("Try on avatar error:", error);
res.status(500).json({
success: false,
error: error.message || "Failed to equip outfit on avatar",
});
}
});
export default router;