0%

2019年4月18日在steam发布了个人一款独立动作游戏《EndlessHell》,本文将借此记录发布steam游戏的流程

游戏链接

UE4版本4.21

SteamWork入门指南

文中内容前半部分为提交当时编辑,搬运至博客后更新后续部分

配置

SteamDLL

复制steam程序中的dll文件至本地引擎文件内*\UE_4.21\Engine\Binaries\ThirdParty\Steamworks\Steamv139

如下图所示

最终效果如下

Engine.ini

添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")

[OnlineSubsystem]
DefaultPlatformService=Steam

[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480//测试ID,正式包用自己的ApplicationID
Achievement_0_Id=ACH_WIN_ONE_GAME //添加成就,正式包用自己的成就
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"

UE4配置

开启子系统,如下图

至此进入游戏后按shift+tab可以唤出steam窗口,记住需要使用==独立窗口模式运行==

  • 如需使用steamapi,在target.cs中开启如下选项
1
bUsesSteam = true;
  • 同时在build.cs中如下模块
1
2
3
4
5
6
7
PublicDependencyModuleNames.AddRange(new string[] {
"OnlineSubsystem",
"OnlineSubsystemUtils",
"Steamworks"
});

DynamicallyLoadedModuleNames.Add("OnlineSubsystemSteam");

SteamWork

  • 首先需要注册一个stemawork账号
  • 发布steam游戏需要申请一个ID,本作ID是**1055000**(此数字+1为depot的ID),以下用的此数字均为游戏ID,另外需要一次性付费100美刀(当时价格),当销售额度达到1000美刀后返还
  • 另外需要提供个人银行账户,每月月初当上个月份税后金额达到100刀以上会打款到账户
  • 在进入下面之前需要先下载steamSDK,如steamwork首页右下角位置,如图

应用管理

应用程序

通用

设置正确的游戏名字和平台

steam输入

提供的输入方式,如手柄,xbox等

SteamPipe

提交版本

这个是版本生成器,用于提交版本和选择当前在商店下载的游戏版本

提交版本可以使用SDK包中的工具提交,在开始提交之前需要设置如下内容

  1. 修改 sdk\tools\ContentBuilder\scripts\app_build_*.vdf文件为app_build_1055000.vdf,同时修改内容为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"appbuild"
{
"appid" "1055000"
"desc" "Your build description here" // description for this build
"buildoutput" "..\output\" // build output folder for .log, .csm & .csd files, relative to location of this file
"contentroot" "..\content\" // root content folder, relative to location of this file
"setlive" "" // branch to set live after successful build, non if empty
"preview" "0" // to enable preview builds
"local" "" // set to flie path of local content server

"depots"
{
"1055001" "depot_build_1055001.vdf"
}
}

必须修改的是appid和depots的内容, 对应的ID修改成自己的

  1. depot_build_1055001.vdf文件同样修改内容为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"DepotBuildConfig"
{
// Set your assigned depot ID here
"DepotID" "1055001"

// Set a root for all content.
// All relative paths specified below (LocalPath in FileMapping entries, and FileExclusion paths)
// will be resolved relative to this root.
// If you don't define ContentRoot, then it will be assumed to be
// the location of this script file, which probably isn't what you want
"ContentRoot" "E:\Download\steamworks_sdk_144\sdk\tools\ContentBuilder\content"

// include all files recursivley
"FileMapping"
{
// This can be a full path, or a path relative to ContentRoot
"LocalPath" "*"

// This is a path relative to the install folder of your game
"DepotPath" "."

// If LocalPath contains wildcards, setting this means that all
// matching files within subdirectories of LocalPath will also
// be included.
"recursive" "1"
}

// but exclude all symbol files
// This can be a full path, or a path relative to ContentRoot
"FileExclusion" "*.pdb"
}

主要修改depotsid和content的路径

  1. 运行 \sdk\tools\ContentBuilder\builder\steamcmd.exe准备提交内容

  2. ```\sdk\tools\ContentBuilder\content``内是游戏包的内容,请确保exe启动名字与==[安装>通用安装>启动选项>可执行文件]==名字一样

  3. 更新>登录>运行如下脚本提交

    1
    run_app_build E:\Download\steamworks_sdk_144\sdk\tools\ContentBuilder\scripts\app_build_1055000.vdf

    路径为app_build文件的路径

  4. 提交后看到如下图的新的分支,修改default为当前展示和下载的分支

depot设置

此页面比较简单,需要正确设置程序支持的语言平台

安装

通用安装
安装文件夹

下载安装以后的文件夹名字

启动选项
  • 可执行文件:启动程序,一般就是引擎打包以后那个exe文件
  • 其他正确设置就可以
可再发行文件

Steam 可以自动安装您的许多常见可再发行文件

客户端图像

根据要求规格上传各种icon文件

统计与成就

统计

设置统计的类型和名称

成就

设置名称、描述和icon

本地化

成就名称和描述的本地化,可以下载文档再提交更新

管理序列号

申请序列号,注意其中不同的序列号的用途

在首页有下载选项

发布

所有修改完后都需要进入发布选项进行发布到商店,系统会提示差异

成就API

  • 很容易被忘记的一点,ini文件利必须设置与网站成就对应的字符串,如下图

  • 蓝图api

读写成就之前必须先缓存成功

目前版本对于成就进度条的读取和设置只有0或者1,所以只要写入的参数大于0成就即完成,不知后续更新能不能完善

蓝图方法DrawDebugLine(以及其他图形)的方法只能在开发者模式中使用

下文为了得到发布版的DrawLine之类的功能

LineBatchComponent

  • 在UWorld中看到如下声明
1
2
3
4
5
6
7
8
9
10
11
/** Line Batchers. All lines to be drawn in the world. */
UPROPERTY(Transient)
class ULineBatchComponent* LineBatcher;

/** Persistent Line Batchers. They don't get flushed every frame. */
UPROPERTY(Transient)
class ULineBatchComponent* PersistentLineBatcher;

/** Foreground Line Batchers. This can't be Persistent. */
UPROPERTY(Transient)
class ULineBatchComponent* ForegroundLineBatcher;
  • 然后创建并注册组件
1
2
3
4
5
6
7
8
9
10
11
12
*****
if(!LineBatcher)
{
LineBatcher = NewObject<ULineBatchComponent>();
LineBatcher->bCalculateAccurateBounds = false;
}

if(!LineBatcher->IsRegistered())
{
LineBatcher->RegisterComponentWithWorld(this);
}
****
  • 再到组件LineBatchComponent.h

  • 先关注组件内部申明的类FLineBatcherSceneProxy : public FPrimitiveSceneProxy

    • 这个代理继承自FPrimitiveSceneProxy,这个类可以得到重要的FPrimitiveDrawInterface,通过这个类可以画出需要的图形(其他各类方法基本都是通过PDI来画出图形),代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//重写的方法,不需要自己调用
void FLineBatcherSceneProxy::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const
{
QUICK_SCOPE_CYCLE_COUNTER( STAT_LineBatcherSceneProxy_GetDynamicMeshElements );

for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
if (VisibilityMap & (1 << ViewIndex))
{const FSceneView* View = Views[ViewIndex];
//得到PDI
FPrimitiveDrawInterface* PDI = Collector.GetPDI(ViewIndex);

for (int32 i = 0; i < Lines.Num(); i++)
{
//通过PDI画线,Lines是TArray<FBatchedLine>私有成员
PDI->DrawLine(Lines[i].Start, Lines[i].End, Lines[i].Color, Lines[i].DepthPriority, Lines[i].Thickness);
}

for (int32 i = 0; i < Points.Num(); i++)
{
//同理画点
PDI->DrawPoint(Points[i].Position, Points[i].Color, Points[i].PointSize, Points[i].DepthPriority);
}
  • 代理构造的时候提供ULineBatchComponent
  • 目的是把ULineBatchComponent的数据传给此代理
1
2
3
4
5
6
FLineBatcherSceneProxy::FLineBatcherSceneProxy(const ULineBatchComponent* InComponent) :
FPrimitiveSceneProxy(InComponent), Lines(InComponent->BatchedLines),
Points(InComponent->BatchedPoints), Meshes(InComponent->BatchedMeshes)
{
bWillEverBeLit = false;
}
  • 下面到ULineBatchComponent
  • ::DrawLine方法把FBatchedLine添加到数组,然后通过Tick递减时间并删除
1
2
3
4
5
6
7
8
void ULineBatchComponent::DrawLine(const FVector& Start, const FVector& End, const FLinearColor& Color, uint8 DepthPriority, const float Thickness, const float LifeTime)
{
//构造一个FBatchedLine成员
new(BatchedLines) FBatchedLine(Start, End, Color, LifeTime, Thickness, DepthPriority);

// LineBatcher and PersistentLineBatcher components will be updated at the end of UWorld::Tick
MarkRenderStateDirty();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void ULineBatchComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
bool bDirty = false;
// Update the life time of batched lines, removing the lines which have expired.
for(int32 LineIndex=0; LineIndex < BatchedLines.Num(); LineIndex++)
{
FBatchedLine& Line = BatchedLines[LineIndex];
if (Line.RemainingLifeTime > 0.0f)
{
Line.RemainingLifeTime -= DeltaTime;
if(Line.RemainingLifeTime <= 0.0f)
{
// The line has expired, remove it.
BatchedLines.RemoveAtSwap(LineIndex--);
bDirty = true;
}
}
}
*************************

DrawDebug版本

通过world得到相应的LineBatcher然后调用DrawLine方法

1
2
3
4
5
6
7
8
9
if (GEngine->GetNetMode(InWorld) != NM_DedicatedServer)
{
// this means foreground lines can't be persistent
ULineBatchComponent* const LineBatcher = GetDebugLineBatcher( InWorld, bPersistentLines, LifeTime, (DepthPriority == SDPG_Foreground) );
if(LineBatcher != NULL)
{
float const LineLifeTime = GetDebugLineLifeTime(LineBatcher, LifeTime, bPersistentLines);
LineBatcher->DrawLine(LineStart, LineEnd, Color, DepthPriority, Thickness, LineLifeTime);

封装蓝图函数库

FlibDrawGraphics.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "FlibDrawGraphics.generated.h"

/**
*
*/
class ULineBatchComponent;
UCLASS()
class GWORLD_API UFlibDrawGraphics : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
private:
static ULineBatchComponent* GetDebugLineBatcher(const UWorld* InWorld, bool bPersistentLines, float LifeTime, bool bDepthIsForeground);

public:
UFUNCTION(BlueprintCallable)
static void G_DrawLine(UObject* context, FVector Start, FVector End, FLinearColor Color, uint8 DepthPriority, float Thickness, float Duration, bool bPersistent = false);
UFUNCTION(BlueprintCallable)
static void G_DrawBox(UObject* context, FVector Center, FVector Box, FLinearColor Color, uint8 DepthPriority, float Thickness, float Duration, bool bPersistent = false);
UFUNCTION(BlueprintCallable, meta = (context = "WorldContextObject"))
static void G_DrawSoildBox(UObject* context, FVector Center, FVector Extent, FLinearColor Color, uint8 DepthPriority, float Duration, bool bPersistent = false);


};

FlibDrawGraphics.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include "FlibDrawGraphics.h"
#include "Components/LineBatchComponent.h"
#include "Engine/World.h"

ULineBatchComponent* ULibGameFunc::GetDebugLineBatcher(const UWorld* InWorld, bool bPersistentLines, float LifeTime, bool bDepthIsForeground)
{

return (InWorld ? (bDepthIsForeground ? InWorld->ForegroundLineBatcher : ((bPersistentLines || (LifeTime > 0.f)) ? InWorld->PersistentLineBatcher : InWorld->LineBatcher)) : NULL);

}


//画线
void ULibGameFunc::BP_DrawLine(UObject* context, FVector Start, FVector End, FLinearColor Color, uint8 DepthPriority, float Thickness, float Duration, bool bPersistent /*= false*/)
{
UWorld* world = context->GetWorld();
if (world)
{
//通过优先级得到对应的组件
ULineBatchComponent* const LineBatcher = GetDebugLineBatcher(world, bPersistent, Duration, DepthPriority == SDPG_Foreground);
//如果是永久的设置时间-1
float const ActualLifetime = bPersistent ? -1.0f : ((Duration > 0.f) ? Duration : LineBatcher->DefaultLifeTime);
LineBatcher->DrawLine(Start, End, Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
}
}


//画盒子边框,通过8个点然后DrawLine得到
void ULibGameFunc::BP_DrawBox(UObject* context, FVector Center, FVector Box, FLinearColor Color, uint8 DepthPriority, float Thickness, float Duration, bool bPersistent /*= false*/)
{

UWorld* world = context->GetWorld();
if (world)
{

ULineBatchComponent* const LineBatcher = GetDebugLineBatcher(world, bPersistent, Duration, DepthPriority == SDPG_Foreground);
float const ActualLifetime = bPersistent ? -1.0f : ((Duration > 0.f) ? Duration : LineBatcher->DefaultLifeTime);
if (LineBatcher)
{ LineBatcher->DrawLine(Center + FVector(Box.X, Box.Y, Box.Z), Center + FVector(Box.X, -Box.Y, Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(Box.X, -Box.Y, Box.Z), Center + FVector(-Box.X, -Box.Y, Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(-Box.X, -Box.Y, Box.Z), Center + FVector(-Box.X, Box.Y, Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(-Box.X, Box.Y, Box.Z), Center + FVector(Box.X, Box.Y, Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(Box.X, Box.Y, -Box.Z), Center + FVector(Box.X, -Box.Y, -Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(Box.X, -Box.Y, -Box.Z), Center + FVector(-Box.X, -Box.Y, -Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(-Box.X, -Box.Y, -Box.Z), Center + FVector(-Box.X, Box.Y, -Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(-Box.X, Box.Y, -Box.Z), Center + FVector(Box.X, Box.Y, -Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(Box.X, Box.Y, Box.Z), Center + FVector(Box.X, Box.Y, -Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(Box.X, -Box.Y, Box.Z), Center + FVector(Box.X, -Box.Y, -Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(-Box.X, -Box.Y, Box.Z), Center + FVector(-Box.X, -Box.Y, -Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);
LineBatcher->DrawLine(Center + FVector(-Box.X, Box.Y, Box.Z), Center + FVector(-Box.X, Box.Y, -Box.Z), Color.ToFColor(true), DepthPriority, Thickness, ActualLifetime);

}
}

}

//画实体的盒子
void ULibGameFunc::DrawSoildBox(UObject* context, FVector Center, FVector Extent, FLinearColor Color, uint8 DepthPriority, float Duration, bool bPersistent/*=false*/)
{
FBox Box = FBox::BuildAABB(Center, Extent);

if (context)
{
UWorld* World = context->GetWorld();
if (World)
{

ULineBatchComponent* const LineBatcher = GetDebugLineBatcher(World, bPersistent, Duration, DepthPriority == SDPG_Foreground);
if (LineBatcher != NULL)
{

float const ActualLifetime = bPersistent ? -1.0f : ((Duration > 0.f) ? Duration : LineBatcher->DefaultLifeTime);
LineBatcher->DrawSolidBox(Box, FTransform::Identity, Color.ToFColor(true), DepthPriority, ActualLifetime);
}
}

}
}

蓝图测试

本文介绍从max到UE4顶点动画实现流程

使用的方式是贴图记录顶点位置和法线信息通过材质球使静态模型模拟动画的播放

Alt text

脚本下载

顶点缓存脚本

  • 如果是非骨骼动画,可以直接跳过此步骤

用顶点缓存脚本把骨骼动画信息保存到==pointCache修改器==内,脚本界面如下

Alt text

  • 缓存文件可以选择自定义路径
  • 烘培范围默认是时间轴的时间,一般不需要自定义

完成后如下图所示

Alt text

skin修改器会被自动禁用,然后就可以进入下一步骤

用顶点动画导出工具

脚本界面如下

Alt text

  • 动画设置的的开始和结束一般不用修改,间隔参数后有详细数据测试
  • 压缩格式和渲染通道不建议修改
  • 输出格式可以用FLOAT16减少体积,测试影响不大

点击【开始烘培】后选择目录保存2张贴图,然后模型窗口自动生成后缀为MorpohExport的模型,选择导出【非动画模型】,得到如下图

Alt text

注意
  1. 每个动作都需要一个FBX和2个贴图
测试间隔参数

模型顶点数为:1729

  1. 以Run动作为测试,模型资源大小:189kb
    1. 原始数据:25帧,2张贴图大小合计(非cook数据,以下简称贴图):273kb
    2. 间隔1帧:13帧,贴图:143kb
    3. 间隔2帧:9帧数,贴图:99kb
    4. 间隔3帧:7帧,贴图:76kb
    5. 间隔4帧:5帧,贴图:60kb
    6. 间隔5帧:5帧,贴图:53kb
  2. 以attack动作测试,模型资源大小:189kb
    1. 原始数据:91帧,贴图:962kb
    2. 间隔1帧:46帧,贴图:501kb
    3. 间隔2帧:31帧数,贴图:343kb
    4. 间隔3帧:23帧,贴图:262kb
    5. 间隔4帧:19帧,贴图:214kb
    6. 间隔5帧:16帧,贴图:180kb
结论
  1. 原始动画的帧数直接决定贴图y轴尺寸(像素数量),意味着帧数越少贴图越小(线性)
  2. 原始动画的顶点数决定贴图x轴,所以尽可能的降低顶点数,同样意味着顶点数越少贴图越小
  3. 由于顶点材质球(具体见下面材质球部分)计算有过度效果,所以去掉中间帧数(设置间隔参数)带来的影响并不大,损失部分细节,但不会出现跳跃现象,但是资源大小是线性变化的
  4. 从max中对动画进行帧数手动减半,效果等同于用脚本设置间隔参数1,而且效果更直接,方便观察和调整

UE4部分

导入模型

  • 如图所示选项需要设置
EXR贴图

  • 参数设置如图
    • sRGB需要去掉
    • 压缩格式设置成HDR
Normal贴图

  • 参数设置
    • sRGB去掉
    • 压缩格式设置为VectorDisplacementMap
    • 其他不是很关键
材质球

  • 使用UE自带的材质函数
    • 设置材质球的TangentSpaceNormal选项为false
    • NumCustomizedUVs设置为4
    • 图中sp节点为动画速度
    • Mor参数为动画帧数
      • 测试发现参数不能小于帧数,也就是贴图的Y轴像素大小
      • 当帧数过小,比如5帧的时候,设置这个Mor参数为5的较大整数倍,比如100,可以去掉原本有的跳帧显现

脚本核心功能解释

顶点缓存脚本
  • max自带顶点缓存插件,本脚本工作原理是调用了插件的功能
  • 核心代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

createDialog test_dialog

)

--Label RangeSelect "选择导出范围"
checkbox cbCustomRange "自定义范围" checked:false tooltip:"默认使用时间轴范围"
spinner Start "开始帧:" range:[-10000,10000,0] type:#integer enabled:false
Spinner End "结束帧:" range:[-10000,10000,100] type:#integer enabled:false

on cbCustomRange changed stat do
(
if cbCustomRange.checked==true then
(
Start.enabled=true
End.enabled=true
)
else
(
Start.enabled=false
End.enabled=false
)
)


)

Button _BakePC "开始缓存" width:130 height:64
progressbar Bake_prog color:green
on _BakePC pressed do
(
if PCpath.text=="" then
messagebox "请选择输出文件夹以保存顶点缓存文件"
else
if $==undefined then
messagebox"选择烘培物体"
else
(
for i = 1 to selection.count do --遍历所有选择的物体,一般只会选择1个
(
A= selection as array
OBJname = A[i].name + ".xml"
FilePathName= PCpath.text
PointCacheName= FilePathName +@"\"+ OBJname
addmodifier A[i] (Point_Cache ()) --添加点缓存修改器
A[i].modifiers[#Point_Cache].filename=PointCacheName
if cbCustomRange.checked then --如果开启了自定义,就设置插件内如下参数
(
A[i].modifiers[#Point_Cache].playbackType=2
A[i].modifiers[#Point_Cache].playbackStart=Start.value
A[i].modifiers[#Point_Cache].playbackEnd=End.value
)
else
(
A[i].modifiers[#Point_Cache].playbackType=0
)

cacheOps.recordcache A[i].modifiers[#point_cache] --记录点信息
cacheOps.DisableBelow A[i].modifiers[#point_cache] --关闭下面的修改器,一般就是skin

Bake_prog.value = 100.*i/A.count


)

Messagebox "生成完毕"


)
)



)
createdialog PointCacheTool


顶点动画导出工具
  • 修改自Epic的顶点动画脚本,原版插件多数功能无用而且无导出选项
  • 核心代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn renderOutTheTextures = (	

fopenexr.SetCompression CompressionType --压缩格式,一般不压缩
print("输出压缩格式:"+(CompressionType as string))
fopenexr.setLayerOutputType 0 OutputType -- 输出通道,一般就是RPG
print("输出通道:"+(OutputType as string))
fopenexr.setLayerOutputFormat 0 OutputFormat --输出格式,一般FLOAT16够用
global TextureName = getSaveFileName types:"EXR (*.EXR)|*.EXR"
if TextureName == undefined then (
messagebox "需要选择一个路径"
)
else(
uvString="_UV"+((targetMorphUV-1) as string)
TextureNameNormal= replace TextureName (findString TextureName ".EXR") 4 (uvString+"_Normals.BMP")
TextureNameOffset= replace TextureName (findString TextureName ".EXR") 4 (uvString+".EXR")
global FinalTexture = bitmap numberofVerts (MorphVertOffsetArray.count) filename:TextureNameOffset hdr:true;
global FinalMorphTexture = bitmap numberofVerts (MorphVertOffsetArray.count) filename:TextureNameNormal hdr:true gamma:1.0 ;
for i=0 to (MorphVertOffsetArray.count-1) do (
setPixels FinalTexture [0, i] MorphVertOffsetArray[(i+1)]
setPixels FinalMorphTexture [0, i] MorphNormalArray[(i+1)] --设置图片对应坐标的像素颜色 2d坐标X分量是列,Y分量是行
--设置图片对应坐标的像素颜色 2d坐标X分量是列,Y分量是行
)
save FinalTexture gamma:1.0
close FinalTexture

save FinalMorphTexture gamma:1.0
close FinalMorphTexture
)
)

开发环境配置流程

MXSPyCom
  1. 下载GitHub的MXSPyComRelease包,链接
  2. 解压,并将安装包内的MXSPyCOM.exe文件复制到自定义目录
  3. initialize_COM_server.ms脚本放到max对应版本的startup目录中,默认路径是%localappdata%\autodesk\3dsmax\scripts\startup
VsCode
  1. 下载LanguageMaxScript插件

  2. 设置工作区

  3. 创建/配置*task.json**,可以ctrl+e***搜索框中输入>task搜索到Configure Default Build Task,在文件内设置如下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "version": "2.0.0",
    "tasks": [
    {
    "label": "Execute in Max",
    "type": "process",
    "command": "C:/MXSPyCOM.exe", //MXSPyCom.exe的路径
    "args": ["-f", "${file}"],
    "presentation": {
    "reveal": "always",
    "panel": "new"
    }
    }
    ]
    }

  4. 更改快捷键配置,方便调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   [
{
"key": "ctrl+e",
"command": "workbench.action.tasks.runTask",
"args": "Execute in Max" //对应任务的名称
},
{
"key": "shift+e",
"command": "workbench.action.quickOpen" //原来的ctrl+e功能
},
{
"key": "shift+e",
"command": "workbench.action.quickOpenNavigateNextInFilePicker",
"when": "inFilesPicker && inQuickOpen"
}
]


测试脚本
  1. 在工作目录创建后缀为 ms格式的脚本文件
  2. 启动max
  3. 复制如下代码到脚本文件内,按 ctrl+e 调试
1
2
3
4
5
6
7
8
9
10
11
12
13
rollout rename_rollout "Enter New Base Name"
(
edittext base_name ""
button rename_them "RENAME"
On rename_them pressed do
(
if base_name.text!="" do (
for i in selection do i.name=uniqueName base_name.text
)
)
)
CreateDialog rename_rollout 250 50

  1. max生成如下窗口