3dsmax2026插件开发入门:使用.net8开发

环境:

  • window11
  • 3dsmax2026
  • .net8
  • vs2026

写这个的原因:

看到autodesk官网上明确声明:3dmax2026的一项重大变更是.net的支持从4.8转向了.net8,但网络并没有看到相关的入门博客,问chatgpt耗费了我1个多小时,最后还让我用c++开发。。。

最后,我只能扒官方的文档一步步尝试,还好最后成功了。

参考:

第一个例子:编写一个跟随3dsmax启动的插件

代码:

MyFirst3dMaxPlugin.slnx

xml 复制代码
<Solution>
  <Configurations>
    <Platform Name="Any CPU" />
    <Platform Name="x64" />
  </Configurations>
  <Project Path="MyFirst3dMaxPlugin/MyFirst3dMaxPlugin.csproj">
    <Platform Solution="*|x64" Project="x64" />
  </Project>
</Solution>

MyFirst3dMaxPlugin.csproj

xml 复制代码
<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<TargetFramework>net8.0-windows</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
		<Platforms>x64</Platforms>
	</PropertyGroup>

	<ItemGroup>
		<!-- 修改为自己3dmax的安装目录 -->
		<Reference Include="Autodesk.Max">
			<HintPath>D:\AutoDesk\3ds Max 2026\Autodesk.Max.dll</HintPath>
		</Reference>
	</ItemGroup>

</Project>

AssemblyFunctions.cs

csharp 复制代码
using System.Reflection;

namespace MyFirst3dMaxPlugin
{
    public class AssemblyFunctions
    {
        //下面是四个3dmax支持的声明周期函数
        //注意: 我是向d盘写入文件, 如果向c盘写入可能会有权限的问题

        public static void AssemblyMain()
        {
            File.AppendAllText("d:\\demo.txt", "AssemblyMain-" + DateTime.Now.ToString() + "\r\n");
            // 如果想在3dmax启动的时候调试的话 加上下面一行, 这样3dmax启动的时候会有弹框提示
            //System.Diagnostics.Debugger.Launch();
        }

        public static void AssemblyShutdown()
        {
            File.AppendAllText("d:\\demo.txt", "AssemblyShutdown-" + DateTime.Now.ToString() + "\r\n");
        }

        public static void AssemblyLoad()
        {
            File.AppendAllText("d:\\demo.txt", "AssemblyLoad-" + DateTime.Now.ToString() + "\r\n");
        }
        public static void AssemblyInitializationCleanup()
        {
            File.AppendAllText("d:\\demo.txt", "AssemblyInitializationCleanup-" + DateTime.Now.ToString() + "\r\n");
        }
    }
}

编译运行

编译后直接放到3dmax的bin/assemblies目录:

可以将 ·MyFirst3dMaxPlugin.deps.json· 一并拷贝过去

这样打开3dmax后就可以看到写入的日志了:

当我们关掉3dmax后再看日志:

第二个例子:写一个内嵌的web服务器

在第一个例子基础上,在3dsmax2026启动时就开启一个web服务器。

代码:

改造 MyFirst3dMaxPlugin.csproj

xml 复制代码
<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<TargetFramework>net8.0-windows</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
		<Platforms>x64</Platforms>
	</PropertyGroup>

	<ItemGroup>
		<!-- 修改为自己3dmax的安装目录 -->
		<Reference Include="Autodesk.Max">
			<HintPath>D:\AutoDesk\3ds Max 2026\Autodesk.Max.dll</HintPath>
		</Reference>
		<!-- web框架 -->
		<FrameworkReference Include="Microsoft.AspNetCore.App" />
	</ItemGroup>

</Project>

新增 Program.cs

csharp 复制代码
using Microsoft.AspNetCore.Builder;

namespace MyFirst3dMaxPlugin
{
    public class Program
    {
        public static void Main()
        {
            var builder = WebApplication.CreateBuilder();
            var app = builder.Build();
            app.MapGet("/", () =>
            {
                return "hello-" + DateTime.Now;
            });
            app.Run("http://localhost:5000");
        }
    }
}

改造 AssemblyFunctions.cs

csharp 复制代码
using System.Reflection;

namespace MyFirst3dMaxPlugin
{
    public class AssemblyFunctions
    {
        //下面是四个3dmax支持的声明周期函数
        //注意: 我是向d盘写入文件, 如果向c盘写入可能会有权限的问题

        public static void AssemblyMain()
        {
            File.AppendAllText("d:\\demo.txt", "AssemblyMain-" + DateTime.Now.ToString() + "\r\n");
            // 如果想在3dmax启动的时候调试的话 加上下面一行, 这样3dmax启动的时候会有弹框提示
            //System.Diagnostics.Debugger.Launch();

            // 第一种方法: 程序集解析事件
            AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
            {
                string assemblyName = new AssemblyName(args.Name).Name;
                string depsPath = Path.Combine(
                    Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
                    "MyFirst3dMaxPlugin-Dependency",
                    assemblyName + ".dll"
                );

                if (File.Exists(depsPath))
                    return Assembly.LoadFrom(depsPath);

                return null;
            };

            //// 第二种方法: 手动加载所有dll
            //string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            //string depsDir = Path.Combine(assemblyDir, "MyFirst3dMaxPlugin-Dependency");

            //// 加载所有依赖DLL
            //foreach (string dll in Directory.GetFiles(depsDir, "*.dll"))
            //{
            //    try
            //    {
            //        Assembly.LoadFrom(dll);
            //    }
            //    catch (Exception ex)
            //    {
            //        // 处理加载失败
            //        File.AppendAllText("d:\\demo.txt", "AssemblyMain-处理加载失败-" + dll + "-" + DateTime.Now.ToString() + "\r\n");
            //    }
            //}

            Task.Run(() => Program.Main());
        }

        public static void AssemblyShutdown()
        {
            File.AppendAllText("d:\\demo.txt", "AssemblyShutdown-" + DateTime.Now.ToString() + "\r\n");
        }

        public static void AssemblyLoad()
        {
            File.AppendAllText("d:\\demo.txt", "AssemblyLoad-" + DateTime.Now.ToString() + "\r\n");
        }
        public static void AssemblyInitializationCleanup()
        {
            File.AppendAllText("d:\\demo.txt", "AssemblyInitializationCleanup-" + DateTime.Now.ToString() + "\r\n");
        }
    }
}

编译运行

发布配置:

将 Pulish 下的内容整体拷贝到 bin/assemblies\MyFirst3dMaxPlugin-Dependency

还有将 MyFirst3dMaxPlugin.dll 拷贝到 bin/assemblies 下

现在让我们再打开3dsmax2026,然后看下浏览器:

现在,一个内嵌的web服务器已经诞生了。

第三个例子:写一个自定义菜单项

目标:点击这个菜单后向场景中添加一个茶壶

代码:

新建一个工程ActionPluginDemo,直接上代码:
ActionPluginDemo.csproj

csharp 复制代码
<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<TargetFramework>net8.0-windows</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
		<Platforms>x64</Platforms>
		<!-- 修改为自己3dmax的安装目录 -->
		<ADSK_3DSMAX_x64_2026>D:\AutoDesk\3ds Max 2026</ADSK_3DSMAX_x64_2026>
	</PropertyGroup>

	<ItemGroup>
		<Reference Include="Autodesk.Max">
			<HintPath>$(ADSK_3DSMAX_x64_2026)\Autodesk.Max.dll</HintPath>
			<Private>False</Private>
		</Reference>
		<Reference Include="CSharpUtilities">
			<HintPath>$(ADSK_3DSMAX_x64_2026)\CSharpUtilities.dll</HintPath>
			<Private>False</Private>
		</Reference>
		<Reference Include="ManagedServices">
			<HintPath>$(ADSK_3DSMAX_x64_2026)\ManagedServices.dll</HintPath>
			<Private>False</Private>
		</Reference>
		<Reference Include="MaxCustomControls">
			<HintPath>$(ADSK_3DSMAX_x64_2026)\MaxCustomControls.dll</HintPath>
			<Private>False</Private>
		</Reference>
		<Reference Include="UiViewModels">
			<HintPath>$(ADSK_3DSMAX_x64_2026)\UiViewModels.dll</HintPath>
			<Private>False</Private>
		</Reference>
		<Reference Include="WPFCustomControls">
			<HintPath>$(ADSK_3DSMAX_x64_2026)\WPFCustomControls.dll</HintPath>
			<Private>False</Private>
		</Reference>
	</ItemGroup>

	<!-- 自动将生成的 ActionPluginDemo.dll 拷贝到 bin\assemblies -->
	<Target Name="PostBuild" AfterTargets="PostBuildEvent">
		<Copy SourceFiles="$(TargetPath)"
			DestinationFolder="$(ADSK_3DSMAX_x64_2026)\bin\assemblies\"
			SkipUnchangedFiles="true" />
	</Target>

</Project>

MaxDotNetCUIDemo.cs

csharp 复制代码
using Autodesk.Max;
using UiViewModels.Actions;

namespace ActionPluginDemo
{
    public class MaxDotNetCUIDemo : CuiActionCommandAdapter
    {
        // 动作显示的文本
        public override string ActionText
        {
            get { return InternalActionText; }
        }

        // 动作所属的分类
        public override string Category
        {
            get { return InternalCategory; }
        }

        // 执行动作时的逻辑
        public override void Execute(object parameter)
        {
            var global = GlobalInterface.Instance;
            var coreInterface = global.COREInterface13;

            try
            {
                // 创建茶壶几何体
                IClass_ID teapotClassId = global.Class_ID.Create(
                    (uint)BuiltInClassIDA.TEAPOT_CLASS_ID,
                    (uint)BuiltInClassIDB.TEAPOT_CLASS_ID);

                // 创建几何体实例
                object obj = coreInterface.CreateInstance(
                    SClass_ID.Geomobject,
                    teapotClassId as IClass_ID);

                if (obj == null)
                {
                    coreInterface.PushPrompt("无法创建茶壶对象");
                    return;
                }

                // 将对象转换为 IObject
                var teapotObject = obj as IObject;
                if (teapotObject == null)
                {
                    coreInterface.PushPrompt("无法将对象转换为 IObject");
                    return;
                }

                // 创建场景节点
                IINode node = global.COREInterface.CreateObjectNode(teapotObject);

                // 正确的方法:将节点添加到场景中
                coreInterface.RootNode.AttachChild(node, false);

                // 获取参数块
                IAnimatable anim = teapotObject as IAnimatable;
                if (anim != null)
                {
                    IIParamBlock2 pb = anim.GetParamBlock(0);
                    if (pb != null)
                    {
                        // 设置茶壶参数
                        // 参数索引和意义:
                        // 0: 半径 (Radius)
                        // 1: 分段数 (Segments)
                        // 2: 平滑 (Smooth)
                        // 3: 生成贴图坐标 (Generate Mapping Coords)
                        pb.SetValue(0, coreInterface.Time, 25.0f, 0);  // 半径25单位
                        pb.SetValue(1, coreInterface.Time, 4, 1);       // 分段数4
                        pb.SetValue(2, coreInterface.Time, 1, 2);    // 平滑
                        pb.SetValue(3, coreInterface.Time, 0, 3);   // 不生成贴图坐标
                    }
                }

                // 选择新创建的节点
                coreInterface.SelectNode(node, true);

                // 重绘视图
                coreInterface.RedrawViews(coreInterface.Time, RedrawFlags.Normal, null);

                coreInterface.PushPrompt("茶壶创建成功!");
            }
            catch (Exception ex)
            {
                coreInterface.PushPrompt($"Error: {ex.Message}");
            }
        }

        // 内部动作文本
        public override string InternalActionText
        {
            get { return "Jackletter-ActionText!"; }
        }

        // 内部分类名称
        public override string InternalCategory
        {
            get { return "Jackletter-Category"; }
        }
    }
}

编译运行

编译完成后,已自动将 ActionPluginDemo.dll 丢进 bin\assemblies目录,如下:

然后,启动3dmax

配置菜单

启动后,点击 "自定义"-> "菜单编辑器",在弹出的窗口中搜索 "jackletter"(类别切换成所有类别),会看到下面收到的我们定义的插件信息,将它拖拽到右面,然后点击保存,在弹框提示中选择"保存",我们可以在文件弹出的菜单配置文件保存框中写入自己喜欢的名字(我写的是test),主要步骤看下图:

观察菜单效果

配置完成后,我们回到3dsmax软件,点击主菜单"文件",会看到我们的插件,点击它,效果如下:

补充项:如何调试代码

  • 对于3dmax刚启动时的代码
    看代码注释,只要将 System.Diagnostics.Debugger.Launch(); 放开就行了。
  • 对于webapi或其他的代码,可以采用附加到进程的方法,如下:
    快捷键:ctrl+alt+p

    然后打个断点:

    然后浏览器访问以下就可以了:
相关推荐
波波0071 天前
每日一题:中间件是如何工作的?
中间件·.net·面试题
无风听海1 天前
.NET 10之可空引用类型
数据结构·.net
码云数智-园园1 天前
基于 JSON 配置的 .NET 桌面应用自动更新实现指南
.net
无风听海1 天前
.NET 10 之dotnet run的功能
.net
岩屿1 天前
Ubuntu下安装Docker并部署.NET API(二)
运维·docker·容器·.net
码云数智-大飞1 天前
.NET 中高效实现 List 集合去重的多种方法详解
.net
easyboot1 天前
使用tinyply.net保存ply格式点云
.net
张人玉1 天前
WPF 多语言实现完整笔记(.NET 4.7.2)
笔记·.net·wpf·多语言实现·多语言适配
波波0072 天前
Native AOT 能改变什么?.NET 预编译技术深度剖析
开发语言·.net
Crazy Struggle3 天前
.NET 中如何快速实现 List 集合去重?
c#·.net