惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Darknet – Hacking Tools, Hacker News & Cyber Security
博客园_首页
T
The Blog of Author Tim Ferriss
罗磊的独立博客
The Register - Security
The Register - Security
IT之家
IT之家
D
Docker
爱范儿
爱范儿
Apple Machine Learning Research
Apple Machine Learning Research
Hugging Face - Blog
Hugging Face - Blog
博客园 - Franky
L
LangChain Blog
J
Java Code Geeks
博客园 - 三生石上(FineUI控件)
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
K
Kaspersky official blog
Latest news
Latest news
博客园 - 聂微东
酷 壳 – CoolShell
酷 壳 – CoolShell
Cyberwarzone
Cyberwarzone
人人都是产品经理
人人都是产品经理
C
CERT Recently Published Vulnerability Notes
The Hacker News
The Hacker News
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
MongoDB | Blog
MongoDB | Blog
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
Spread Privacy
Spread Privacy
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
D
DataBreaches.Net
Project Zero
Project Zero
H
Hackread – Cybersecurity News, Data Breaches, AI and More
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
C
Check Point Blog
S
Schneier on Security
NISL@THU
NISL@THU
T
Tor Project blog
P
Proofpoint News Feed
N
News and Events Feed by Topic
Last Week in AI
Last Week in AI
N
Netflix TechBlog - Medium
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
U
Unit 42
C
Cybersecurity and Infrastructure Security Agency CISA
T
The Exploit Database - CXSecurity.com
L
LINUX DO - 最新话题
Help Net Security
Help Net Security
Blog — PlanetScale
Blog — PlanetScale

机核

游戏性能旗舰最强之选,一加 Ace 6 至尊版国补到手价2999元起 6元钱自己更换电动车刹车线 《生化危机9:安魂曲》编剧Haris Orkin专访 摸金游戏?音乐游戏!暗区新赛季这把能弹的琴有何来历? 好评国产武侠SRPG《息风谷战略》免费DLC现已推出 | 机核 GCORES 碎片 《生化危机:安魂曲》将于5月实装首个免费更新内容 | 机核 GCORES 新锐东方游戏,谱写世界新章! 沉浸式恋爱视觉小说游戏《心象演算》免费试玩版现已正式上线 | 机核 GCORES 互动影游《代号三国:龙起》上线!穿越三国与曹操并肩、与佳人同行、与权谋博弈! 《老头收集梦想生活》,游戏酒桌会6,录音笔VOL.689 | 机核 GCORES 破界·共生——《白日梦:无限世界》五大核心游戏特点解析 价格已到史低,锐龙5 9600X/锐龙7 9700X正适合抄底 时间循环之旅即刻启程!《归环》一周目测试今日开启 热门在线韩游变魂游,洛奇衍生作能否打破“花瓶”魔咒? LG UltraGear evo 全新高端显示器系列:当“5K”遇见“AI”,不止强大,更懂热爱 反套路三国互动影游《代号三国:龙起》今日上线! 愿望单登记人数突破10万!备受瞩目的“女儿养成游戏” 《梦幻魔法公主》今日于Steam平台上线!限时八折优惠中 《黑神话:悟空》全球音乐会2026巡演将于4月29日12时开票 | 机核 GCORES “Snowguelike”生存肉鸽挖矿新作《蛙穿雪境》公布发售日期,5月7日正式上线 | 机核 GCORES 重塑移动办公、AI创作新境!全新华硕灵耀Air系列、ProArt 骁龙版震撼首发,创芯未来 均分88:《Saros》媒体评分汇总 | 机核 GCORES 上海烛龙公布合作遗迹探险游戏《吉时已到》首支预告片 | 机核 GCORES 首个独立游戏《萝薇日记》已上线Steam! 烛龙新IP《吉时已到》首曝,打造国内首款中式合作遗迹探险游戏 喜加一:《暗黑破坏神Ⅳ》国服现已开启限时免费领取本体活动 | 机核 GCORES 新版《生化危机》电影定于9月18日上映,官方网站现已上线 | 机核 GCORES 《冲就完事模拟器2》“星球大战”联动DLC正式宣布 | 机核 GCORES SteamController将于5月4日发售,售价99美元 | 机核 GCORES 事已至此,内存这么用也算省钱了,“2+1”非对称双通道应用实测 505游戏母公司现已收购《明末:渊虚之羽》IP | 机核 GCORES 基石 手动杂谈12|格斗游戏也能讲好故事 | 机核 GCORES I Love You Mr Snowball 我的向日葵小姐 会比GTA6先发售吗?最硬核的生存游戏《DAYZ》要推新DLC了 周记02:在2026遇到新怪谈 什么硬件,能让游戏Loading界面快速消失? | 机核 GCORES GadioSpec《百年风云世界杯》免费试听集 | 机核 GCORES 百年风云世界杯Vol.1丨足球崛起 | 机核 GCORES 100年前,一群人提前替我们经历了AI恐惧 | 机核 GCORES 《呼啸山庄》2026 ——一辆当代艺术的大卡车冲撞了我的大脑 手游《天穗之咲稻姬:日之香巡灵传》宣布将于7月27日停服 | 机核 GCORES 集结梦之队,征战世界杯,《最佳球会ONLINE》上线Steam 山水绝景随心拼 休闲建造游戏《千里山河录》Steam商店页公开 巫师帽、法袍、魔杖,为什么它们是影视、游戏里的法师必备三件套? 《无鞘信使》-第一章 复古风自动战斗肉鸽《终结之终结》Steam商店页面现已上线 电脑里有一款不破不立的MMO,录音笔VOL.688 | 机核 GCORES 《生化危机》30周年纪念周边发售,这次是真的“保护伞” 可靠耐用+AI全能,惠普战66 2025锐龙版深度体验 《时之书:无尽终章》关卡“大航海时代”全球首次公开 经典名作《乌龙派出所》改编经营模拟游戏《乌龙派出所~阿两的商店街物语~》正式宣布支持简体中文 明日开冲,解锁反套路三国互动影游《代号三国:龙起》即将上线 独立游戏《这是我的宝藏!》已发售~ 降低难度不是唯一解,无压力死亡也是好体验 Netflix官宣新片《普通人》:讲述韩国现代史上的权力风暴 《生化危机:安魂曲》全球销量现已突破700万份 《绝地鸭卫》PC版5月15日正式发售 亡妻回忆录?女性向情感叙事游戏《S-mail》现已正式发售 全新酷黑风格,酷睿Ultra 200S PLUS的高性价比搭档!七彩虹BATTLE-AX B860M-PLUS S WIFI7 V20 超级黑刃主板测评 胖狗 索尼发布了港台地区PS5产品价格调整公告,将于5月1日起实施 二次元怪猎+性感美女!《碧蓝幻想:无尽黄昏》开启Beta公测 这款怪谈类型中式恐怖游戏居然更新了?! 《百日战纪 -最终防卫学园-》改编漫画将于今年冬季开启连载 《如被附身,请致电我们》:匈牙利黑色幽默恐怖小说 《竹屿山房杂部卷五》(译文) 大树 超越引擎 摄影分享丨四月 碎片杂记vol.72 当老式FPS与老式动画技术碰撞出新时代的火花—《神探杰克鼠》 想做独游,如何避免首个项目就褒姒? 在线多人动作游戏《OCTOPinbs》将于5月12日上线Steam! 《酒鬼女神的酒诡》确定将于2026年登陆Steam 《CRYMELIGHT》将于11月5日(周四)正式发售!4月25日开始预购! 钢铁国度MKI部落Evolution,蛮兵部分 【钢铁国度】部落-Primal MK I,蛮兵部分 这是一个高中生用ai跑出来的作品,我自称他为物理神话 钢铁国度MKI部落Metamorphosis,蛮兵部分上 钢铁国度MKI部落Metamorphosis,蛮兵部分下 INDIE Live Expo于4月25日举办:首发9款新作,超200款独立游戏亮相 暗黑卡牌策略新作《魔忌:穷鼠啮狸》发布全新中文试玩版和发售预告片,4月30日正式上线在即 战锤40K长篇小说:变节者・苦难主宰(三)(全书完) 战锤40K长篇小说:变节者・苦难主宰(二) 战锤40K长篇小说:变节者・苦难主宰(一) 英语语言学习丨短语专题1:短语的特点 唯一的EVE战争 【少前同人】【M200】战术人形会梦见音乐会吗 【昏迷3】即将发售,前作主角悉数到场!“恶灵”宋老师化身可操控角色 业内人士:游戏公司“十有八九”使用生成式AI,包括卡普空 欢庆一周年:《光与影:33号远征队》全新纪念艺术图、超值折扣与免费更新同步上线 才刚刚开始呢 【抽奖】《星际卡车司机》推出免费大型更新,四折平史低折扣进行中 四人合作FPS游戏《佣兵猎手》抢先体验重大更新 1 现已上线 Raw Fury新作《深馅地牢 Deep Dish Dungeon》将于今秋加入 XGP 塔防幸存者游戏《魔怪来袭》推出首个 DLC 《饿狼传说:群狼之城》1周年纪念!新DLC“沃尔夫冈·克劳萨”今日参战 猫狗相伴 欢乐闯关 双人合作平台跳跃游戏《猫狗同行》上线Steam商店页 《同行:月球逃脱》(Together: Moon Escape)上线将于明日上线Steam
韩版《传奇 2》源码分析与 Unity 重制(三):客户端渲染
Sou1gh0st · 2023-04-03 · via 机核

专题介绍

该专题将会分析 LOMCN 基于韩版传奇 2,使用 .NET 重写的传奇源码(服务端 + 客户端),分析数据交互、状态管理和客户端渲染等技术,此外笔者还会分享将客户端部分移植到 Unity 和服务端用现代编程语言重写的全过程。

系列文章

概览

在这一篇文章中,我们将开始分析传奇客户端的 2D 渲染管线,了解传奇早期的美术资产设计与渲染流程。

底层图形接口

可能传奇在设计之初没有考虑到跨平台用途,或是为了做到极致性能,开发者直接使用了 Direct 3D 的图形接口进行 2D 渲染管线的开发,在客户端的 Main Form 被加载的时候会进行 D3D 的初始化,开发者封装了 DXManager 来管理 RenderState:

// CMain.cs private void CMain_Load(object sender, EventArgs e) { this.Text = GameLanguage.GameName; try { ClientSize = new Size(Settings.ScreenWidth, Settings.ScreenHeight); DXManager.Create(); SoundManager.Create(); CenterToScreen(); } catch (Exception ex) { SaveError(ex.ToString()); } } ​ // DXManager.cs using SlimDX; using SlimDX.Direct3D9; using Blend = SlimDX.Direct3D9.Blend; ​ public static void Create() { Parameters = new PresentParameters { BackBufferFormat = Format.X8R8G8B8, PresentFlags = PresentFlags.LockableBackBuffer, BackBufferWidth = Settings.ScreenWidth, BackBufferHeight = Settings.ScreenHeight, SwapEffect = SwapEffect.Discard, PresentationInterval = Settings.FPSCap ? PresentInterval.One : PresentInterval.Immediate, Windowed = !Settings.FullScreen, }; ​ Direct3D d3d = new Direct3D(); ​ Capabilities devCaps = d3d.GetDeviceCaps(0, DeviceType.Hardware); DeviceType devType = DeviceType.Reference; CreateFlags devFlags = CreateFlags.HardwareVertexProcessing; ​ if (devCaps.VertexShaderVersion.Major >= 2 && devCaps.PixelShaderVersion.Major >= 2) devType = DeviceType.Hardware; ​ if ((devCaps.DeviceCaps & DeviceCaps.HWTransformAndLight) != 0) devFlags = CreateFlags.HardwareVertexProcessing; ​ if ((devCaps.DeviceCaps & DeviceCaps.PureDevice) != 0) devFlags |= CreateFlags.PureDevice; ​ Device = new Device(d3d, d3d.Adapters.DefaultAdapter.Adapter, devType, Program.Form.Handle, devFlags, Parameters); ​ Device.SetDialogBoxMode(true); ​ LoadTextures(); LoadPixelsShaders(); }

这里有几个点需要注意一下:

  1. 传奇采用了 D3D9,从现在来看这是一个非常古老的、来自 2002 年的 Direct X 版本,目前的 GPU 调试工具似乎都已经不再支持,调试起来比较困难;

  2. 传奇采用了 SlimDX 来实现在 .NET 环境中直接访问 Direct X 接口

渲染循环

在上一篇文章中我们提到,客户端的事件循环会逐帧调用 UpdateEnviroment 和 RenderEnvironment,其中前者用于处理网络数据包和更新状态,后者用于渲染,在这里我们重点来看 RenderEnvironment 的实现。

这里首先做了清屏,然后开启 Scene,这里的 Scene 用于分组和管理每一帧的 Draw Call,然后开启透明度混合,设置 Render Target,提交当前 Scene 的 Draw Call 然后通过 EndScene 提交 Command Buffer,最后通过 Present 进行 swap 上屏:

// CMain.cs private static void RenderEnvironment() { try { if (DXManager.DeviceLost) { DXManager.AttemptReset(); Thread.Sleep(1); return; } ​ DXManager.Device.Clear(ClearFlags.Target, Color.CornflowerBlue, 0, 0); DXManager.Device.BeginScene(); DXManager.Sprite.Begin(SpriteFlags.AlphaBlend); DXManager.SetSurface(DXManager.MainSurface); ​ if (MirScene.ActiveScene != null) MirScene.ActiveScene.Draw(); ​ DXManager.Sprite.End(); DXManager.Device.EndScene(); DXManager.Device.Present(); } catch (Direct3D9Exception ex) { DXManager.DeviceLost = true; } catch (Exception ex) { SaveError(ex.ToString()); ​ DXManager.AttemptRecovery(); } }

这里的 RenderTarget 设置方法 SetSurface 是 DXManager 封装出来的,我们来看一下具体的实现:

// DXManager.cs public static void SetSurface(Surface surface) { if (CurrentSurface == surface) return; ​ Sprite.Flush(); CurrentSurface = surface; Device.SetRenderTarget(0, surface); }

这里实际上就是将 RT0 绑定到目标纹理,传奇没有采用 multi target 模式,因此默认情况下片段着色器的输出将直接通过 RT0 绑定的 Attachment 进行输出。再回去看一眼渲染循环中的代码会发现 RT0 会被绑定到 DXManager.MainSurface,而 MainSurface 实际上就是 BackBuffer,因此渲染内容会在 swap 后直接上屏:

// DXManager.cs private static unsafe void LoadTextures() { Sprite = new Sprite(Device); TextSprite = new Sprite(Device); Line = new Line(Device) { Width = 1F }; ​ MainSurface = Device.GetBackBuffer(0, 0); CurrentSurface = MainSurface; Device.SetRenderTarget(0, MainSurface); ​ // ... }

渲染管线

总的来说传奇的渲染管线设计的还是比较简单的,从大流程上而言主要分为两步渲染:

  1. 分层渲染游戏场景

  2. 渲染游戏的 GUI

// GameScene.cs protected internal override void DrawControl() { // Draw Game Scene if (MapControl != null && !MapControl.IsDisposed) MapControl.DrawControl(); ​ // Draw GUI base.DrawControl(); // ... }

在这里我们首先分析游戏场景的渲染,它是通过调用 MapControl.DrawControl 实现的,通过下面的代码可以发现,传奇设计了帧缓存 ControlTexture,在不需要播放动画和移动物体的情况下可以复用之前的渲染结果:

// GameScene.cs - MapControl protected internal override void DrawControl() { if (!DrawControlTexture) return; ​ if (!TextureValid) CreateTexture(); ​ if (ControlTexture == null || ControlTexture.Disposed) return; ​ float oldOpacity = DXManager.Opacity; ​ DXManager.SetOpacity(Opacity); DXManager.Sprite.Draw(ControlTexture, new Rectangle(0, 0, Settings.ScreenWidth, Settings.ScreenHeight), Vector3.Zero, Vector3.Zero, Color.White); DXManager.SetOpacity(oldOpacity); ​ CleanTime = CMain.Time + Settings.CleanDelay; }

这里的 SetOpacity 实际上是调整 BlendMode,由于 MapControl 在初始化时采用的 Opacity 默认值为 1.0,因此会将 BlendMode 设置为 SourceAlpha InverseSourceAlpha,即通过 dst.rgb = src.rgb * src.a + dst.rgb + (1 - src.a) 进行颜色混合:

public static void SetOpacity(float opacity) { if (Opacity == opacity) return; ​ Sprite.Flush(); Device.SetRenderState(RenderState.AlphaBlendEnable, true); if (opacity >= 1 || opacity < 0) { Device.SetRenderState(RenderState.SourceBlend, SlimDX.Direct3D9.Blend.SourceAlpha); Device.SetRenderState(RenderState.DestinationBlend, SlimDX.Direct3D9.Blend.InverseSourceAlpha); Device.SetRenderState(RenderState.SourceBlendAlpha, Blend.One); Device.SetRenderState(RenderState.BlendFactor, Color.FromArgb(255, 255, 255, 255).ToArgb()); } else { Device.SetRenderState(RenderState.SourceBlend, Blend.BlendFactor); Device.SetRenderState(RenderState.DestinationBlend, Blend.InverseBlendFactor); Device.SetRenderState(RenderState.SourceBlendAlpha, Blend.SourceAlpha); Device.SetRenderState(RenderState.BlendFactor, Color.FromArgb((byte)(255 * opacity), (byte)(255 * opacity), (byte)(255 * opacity), (byte)(255 * opacity)).ToArgb()); } Opacity = opacity; Sprite.Flush(); }

经过上面的分析我们知道,游戏场景的渲染主要分为两步:

  1. 判断 TextureValid 为 false 时,通过 CreateTexture 离屏渲染到 ControlTexture;

  2. 将 ControlTexture 绘制到 MainSurface,采用的透明度混合方式为 SourceAlpha InverseSourceAlpha。

游戏场景渲染

游戏内场景渲染包括以下几个步骤:

  1. 通过 DrawBackground 绘制背景,大部分场景是没有背景的;

  2. 通过 DrawFloor 绘制地图背景;

  3. 通过 DrawObjects 绘制地图前景和游戏内对象;

  4. 通过 DrawLights 绘制光源;

  5. 通过 DrawName 绘制 hover 到掉落物、怪物和玩家的悬浮文字。

这里的核心步骤是 DrawFloor 绘制地图背景,DrawObjects 绘制地图前景和游戏内对象,这三个图层的可视化效果如下:

最终的视觉效果如下:

Tilemap 地图绘制

Direct 3D9 的 Sprite 的 UV 坐标系原点为左上角,向右为 x 轴,向下为 y 轴,传奇采用了 Tilemap 地图,按照从左到右、从上到下的顺序进行绘制,每个 Tile 的固定大小为 48 x 32 px,首先根据分辨率计算 x 和 y 轴方向所需的 Tile 数量,这里的 OffsetX 和 OffsetY 分别是半屏下的 Tile 数量,为了避免出现黑边加上 4 作为 x, y 方向的 half extent:

public const int CellWidth = 48; public const int CellHeight = 32; ​ OffSetX = Settings.ScreenWidth / 2 / CellWidth; OffSetY = Settings.ScreenHeight / 2 / CellHeight - 1; ViewRangeX = OffSetX + 4; ViewRangeY = OffSetY + 4;

下面我们来看 DrawFloor 的实现,在这里一共有三个子图层的绘制,我们先简化代码只看 Back 层的绘制。Tilemap 的绘制采用了双层循环,以用户当前坐标为中心,向左减去 ViewRangeY 得到 minY,向右加上 ViewRangeY 得到 maxY,在内层循环中则是从 minX 迭代到 maxX,因此贴 Tile 的顺序为自上而下、从左到右:

private void DrawFloor() { if (DXManager.FloorTexture == null || DXManager.FloorTexture.Disposed) { DXManager.FloorTexture = new Texture(DXManager.Device, Settings.ScreenWidth, Settings.ScreenHeight, 1, Usage.RenderTarget, Format.A8R8G8B8, Pool.Default); DXManager.FloorSurface = DXManager.FloorTexture.GetSurfaceLevel(0); } ​ Surface oldSurface = DXManager.CurrentSurface; DXManager.SetSurface(DXManager.FloorSurface); DXManager.Device.Clear(ClearFlags.Target, Color.Empty, 0, 0); //Color.Black ​ int index; int drawY, drawX; ​ for (int y = User.Movement.Y - ViewRangeY; y <= User.Movement.Y + ViewRangeY; y++) { if (y <= 0 || y % 2 == 1) continue; if (y >= Height) break; ​ drawY = (y - User.Movement.Y + OffSetY) * CellHeight + User.OffSetMove.Y; //Moving OffSet ​ for (int x = User.Movement.X - ViewRangeX; x <= User.Movement.X + ViewRangeX; x++) { if (x <= 0 || x % 2 == 1) continue; if (x >= Width) break; drawX = (x - User.Movement.X + OffSetX) * CellWidth - OffSetX + User.OffSetMove.X; //Moving OffSet if ((M2CellInfo[x, y].BackImage == 0) || (M2CellInfo[x, y].BackIndex == -1)) continue; index = (M2CellInfo[x, y].BackImage & 0x1FFFFFFF) - 1; Libraries.MapLibs[M2CellInfo[x, y].BackIndex].Draw(index, drawX, drawY); } } ​ // ... ​ DXManager.SetSurface(oldSurface); ​ FloorValid = true; }

需要注意的是循环中的 x, y 实际上是 Tile 坐标,在绘制时我们需要将其换算为屏幕坐标,换算的过程很简单,只需要将 Tile 坐标乘以 CellWH 即可。通过 Tile 坐标可以在 Library 中查询 Cell 信息,获取到图片的 Index 并完成绘制,这里有一个细节是实际的 Tile 分辨率为 96 x 64,是 CellWH 48 x 32 的 2 倍,因此在贴 Tile 时在 x, y 方向都会产生 50% 的覆盖,这应该是为了避免出现裂缝:

遮挡关系处理

通过查看 DrawObjects 的代码我们可以发现在逻辑顺序上是先绘制的地图元素,后绘制的人物,但人物却可以被地图的元素挡住:

private void DrawObjects() { // ... for (int y = User.Movement.Y - ViewRangeY; y <= User.Movement.Y + ViewRangeY + 25; y++) { if (y <= 0) continue; if (y >= Height) break; int drawY = (y - User.Movement.Y + OffSetY + 1) * CellHeight + User.OffSetMove.Y; ​ for (int x = User.Movement.X - ViewRangeX; x <= User.Movement.X + ViewRangeX; x++) { if (x < 0) continue; if (x >= Width) break; int drawX = (x - User.Movement.X + OffSetX) * CellWidth - OffSetX + User.OffSetMove.X; // Draw Tilemap if (blend) { if ((fileIndex > 99) & (fileIndex < 199)) Libraries.MapLibs[fileIndex].DrawBlend(index, new Point(drawX, drawY - (3 * CellHeight)), Color.White, true); else Libraries.MapLibs[fileIndex].DrawBlend(index, new Point(drawX, drawY - s.Height), Color.White, (index >= 2723 && index <= 2732)); } else Libraries.MapLibs[fileIndex].Draw(index, drawX, drawY - s.Height); } ​ // Draw Objects for (int x = User.Movement.X - ViewRangeX; x <= User.Movement.X + ViewRangeX; x++) { if (x < 0) continue; if (x >= Width) break; M2CellInfo[x, y].DrawObjects(); } } // ... }

按照先绘制 Tile,再绘制 Objects 的顺序,理论上 Object 会挡住 Tile 从而产生错误的视觉效果,但实际上 Player 可以正确被景物遮挡:

默认配置下,同样的 z-order 后绘制的 Sprite 会遮挡前面绘制的 Sprite,这里看起来非常反直觉的现象实际上是美术资产上做的 trick,在角色和景物的时候有一个 Y 方向向上的偏移,而景物绘制的时候这个向上偏移的 Y 更大,使得角色实际上被绘制的被同一行的 Tile 要更早:

但这还不是全部仔细观察传奇的地图可以发现,当角色完全被景物挡住时,会有一个半透明的身影被渲染出来以避免玩家看不到自己的角色(仔细看树后的角色):

这是怎么实现的呢?实际上在 DrawObjects 对 Tile 和 Objects 的绘制结束后,会开启 AlphaBlend,将 SourceFactor 指定为 0.4 对玩家的角色进行一次透明度混合:

private void DrawObjects() { // draw Tile / Objects // ... ​ // draw player again with alpha blend factor 0.4 DXManager.Sprite.Flush(); float oldOpacity = DXManager.Opacity; DXManager.SetOpacity(0.4F); ​ MapObject.User.DrawMount(); ​ MapObject.User.DrawBody(); ​ if ((MapObject.User.Direction == MirDirection.Up) || (MapObject.User.Direction == MirDirection.UpLeft) || (MapObject.User.Direction == MirDirection.UpRight) || (MapObject.User.Direction == MirDirection.Right) || (MapObject.User.Direction == MirDirection.Left)) { MapObject.User.DrawHead(); MapObject.User.DrawWings(); } else { MapObject.User.DrawWings(); MapObject.User.DrawHead(); } ​ DXManager.SetOpacity(oldOpacity); }

技能与特效的透明度混合

在上面的渲染中,地图和角色的透明度混合大都采用了 SourceAlpha InverseSourceAlpha 的常规混合方式,而对于场景中的技能和特效,为了让他们变得更亮,需要采用 Additive 的混合方式,即 SourceAlpha One:

Device.SetRenderState(RenderState.AlphaBlendEnable, true); Device.SetRenderState(RenderState.SourceBlend, Blend.SourceAlpha); Device.SetRenderState(RenderState.DestinationBlend, Blend.One);

注意这里的 DestinationBlend 一定要设置为 One,不可以为 InverseSourceAlpha,这是因为资产中的特效的 Alpha 通道在很大的区以内都接近 1,如果采用 InverseSourceAlpha 会在地图上产生黑色背景:

1 / 3

而采用正确的 Additive 方式进行混合,则可以得到明亮的特效:

下一步

在这篇文章中,我们着重分析了传奇客户端基于 Direct 3D9 构建的 2D 渲染管线,以及渲染过程中的一些细节。在接下来的文章中我们将继续深入客户端渲染,分析装备、技能和动画的渲染方式。