• 创建一个窗口,窗口表面和交换链
1.检测交换链是否支持
2.启用交换链扩展
3.选择适当的表面格式
4.查找最佳的可用呈现模式
5.设置交换范围
6.创建窗口表面
7.创建交换链
8.查询交换链支持细节


Vulkan 是一个平台无关的 API,它不能直接和窗口系统交互。
为了将 Vulkan 渲染的图像显示在窗口上,我们需要使用 WSI(Window SystemIntegration) 扩展。
我们首先介绍 VK_KHR_surface 扩展,它通过 VkSurfaceKHR 对象抽象出可供 Vulkan 渲染的表面。
这里我们使用 GLFW 来获取 VkSurfaceKHR 对象。
VK_KHR_surface 是一个实例级别的扩展,它已经被包含在使用glfwGetRequiredInstanceExtensions 函数获取的扩展列表中,所以,我们不需要自己请求这一扩展。
WSI 扩展同样也被包含在 glfwGetRequiredInstanceExtensions 函数获取的扩展列表中,也不需要我们自己请求。
由于窗口表面对物理设备的选择有一定影响,它的创建只能在 Vulkan实例创建之后进行

交换链:

Vulkan 没有默认帧缓冲的概念,它需要一个能够缓冲渲染操作的组件。在 Vulkan 中,这一组件就是交换链。Vulkan 的交换链必须显式地创建,不存在默认的交换链。交换链本质上一个包含了若干等待呈现的图像
的队列。我们的应用程序从交换链获取一张图像,然后在图像上进行渲染操作,完成后,将图像返回到交换链的队列中。交换链的队列的工作方式和它呈现图像到表面的条件依赖于交换链的设置。但通常来说,交换链被用来同步图像呈现和屏幕刷新。

并不是所有的显卡设备都具有可以直接将图像呈现到屏幕的能力。比如,被设计用于服务器的显卡是没有任何显示输出设备的。此外,由于图像呈现非常依赖窗口系统,以及和窗口系统有关的窗口表面,这些并非Vulkan 核心的一部分。使用交换链,我们必须保证 VK_KHR_swapchain设备扩展被启用。为了确保VK_KHR_swapchain 设备扩展被设备支持,我们需要扩展VK_KHR_swapchain 函数检测该扩展是否被支持

示例代码:
//设备扩展列表--检测VK_KHR_swapchain--4
const std::vector<const char*> deviceExtensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
//表示满足需求得队列族--2
struct QueueFamilyIndices{//绘制指令的队列族索引int graphicsFamily = -1;//-1表示没有找到满足需求的队列族//支持表现的队列族索引int presentFamily = -1;bool isComplete(){return graphicsFamily >= 0 && presentFamily>=0;}
};
//查询得到的交换链细节信息:
struct SwapChainSupportDetails {VkSurfaceCapabilitiesKHR capabilities;std::vector<VkSurfaceFormatKHR> formats;std::vector<VkPresentModeKHR> presentModes;
};//尽管 VkSurfaceKHR 对象是平台无关的,但它的创建依赖窗口系统VkSurfaceKHR surface;//窗口表面--4VkQueue presentQueue;//呈现队列--4VkSwapchainKHR swapChain;//交换链--4//交换链的图像句柄,在交换链清除时自动被清除--4std::vector<VkImage> swapChainImages;VkFormat swapChainImageFormat;//交换链图像格式--4VkExtent2D swapChainExtent;//交换链图像范围--4//检测交换链是否支持--4bool checkDeviceExtensionSupport(VkPhysicalDevice device){/**实际上,如果设备支持呈现队列,那么它就一定支持交换链。但我们最好还是显式地进行交换链扩展的检测,然后显式地启用交换链扩展。*///枚举设备扩展列表,检测所需的扩展是否存在uint32_t extensionCount;vkEnumerateDeviceExtensionProperties(device,nullptr,&extensionCount,nullptr);std::vector<VkExtensionProperties> availableExtensions(extensionCount);vkEnumerateDeviceExtensionProperties(device,nullptr,&extensionCount,availableExtensions.data());std::set<std::string> requiredExtensions(deviceExtensions.begin(),deviceExtensions.end());for(const auto& extension : availableExtensions){requiredExtensions.erase(extension.extensionName);}//如果这个集合中的元素为 0,说明我们所需的扩展全部都被满足return requiredExtensions.empty();}//检查设备是否满足需求--2bool isDeviceSuitable(VkPhysicalDevice device){QueueFamilyIndices indices = findQueueFamilies(device);//检测交换链是否支持bool extensionsSupported = checkDeviceExtensionSupport(device);//检测交换链的能力是否满足需求,我们只能在验证交换链扩展可用后查询交换链的细节信息//我们只需要交换链至少支持一种图像格式和一种支持我们的窗口表面的呈现模式bool swapChainAdequate = false;if(extensionsSupported){SwapChainSupportDetails swapChainSupport =querySwapChainSupport(device);swapChainAdequate= !swapChainSupport.formats.empty()&& !swapChainSupport.presentModes.empty();}return indices.isComplete() && extensionsSupported &&swapChainAdequate;}//创建一个逻辑设备--3void createLogicalDevice(){//获取带有图形能力的队列族QueueFamilyIndices indices = findQueueFamilies(physicalDevice);std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;std::set<int> uniqueQueueFamilies = {indices.graphicsFamily,indices.presentFamily};/**目前而言,对于每个队列族,驱动程序只允许创建很少数量的队列,但实际上,对于每一个队列族,我们很少需要一个以上的队列。我们可以在多个线程创建指令缓冲,然后在主线程一次将它们全部提交,降低调用开销。Vulkan需要我们赋予队列一个0.0到1.0之间的浮点数作为优先级来控制指令缓冲的执行顺序。即使只有一个队列,我们也要显式地赋予队列优先级*/float queuePriority = 1.0f;for(int queueFamily : uniqueQueueFamilies){//描述了针对一个队列族我们所需的队列数量。VkDeviceQueueCreateInfo queueCreateInfo = {};queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;queueCreateInfo.queueFamilyIndex=queueFamily;queueCreateInfo.queueCount = 1;queueCreateInfo.pQueuePriorities = &queuePriority;queueCreateInfos.push_back(queueCreateInfo);}//指定应用程序使用的设备特性VkPhysicalDeviceFeatures deviceFeatures = {};//创建逻辑设备相关信息VkDeviceCreateInfo createInfo = {};createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;createInfo.queueCreateInfoCount =static_cast<uint32_t>(queueCreateInfos.size());createInfo.pQueueCreateInfos = queueCreateInfos.data();createInfo.pEnabledFeatures = &deviceFeatures;//启用交换链扩展--4createInfo.enabledExtensionCount=static_cast<uint32_t>(deviceExtensions.size());createInfo.ppEnabledExtensionNames = deviceExtensions.data();if(enableValidationLayers){//以对设备和 Vulkan 实例使用相同地校验层createInfo.enabledLayerCount =static_cast<uint32_t>(validataionLayers.size());createInfo.ppEnabledLayerNames = validataionLayers.data();}else{createInfo.enabledLayerCount = 0;}/**vkCreateDevice 函数的参数:1.创建的逻辑设备进行交互的物理设备对象2.指定的需要使用的队列信息3.可选的分配器回调4.用来存储返回的逻辑设备对象的内存地址逻辑设备并不直接与Vulkan实例交互,所以创建逻辑设备时不需要使用Vulkan实例作为参数*///创建逻辑设备if(vkCreateDevice(physicalDevice,&createInfo,nullptr,&device) != VK_SUCCESS){throw std::runtime_error("failed to create logical device!");}//获取指定队列族的队列句柄//它的参数依次是逻辑设备对象,队列族索引,队列索引,用来存储返回的队列句柄的内存地址//我们只创建了一个队列,所以,可以直接使用索引 0vkGetDeviceQueue(device,indices.graphicsFamily,0,&graphicsQueue);vkGetDeviceQueue(device,indices.presentFamily,0,&presentQueue);}/**来查找合适的交换链设置,设置的内容如下:1.表面格式 (颜色,深度)2.呈现模式 (显示图像到屏幕的条件)3.交换范围 (交换链中的图像的分辨率)*///选择适当的表面格式--4VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats){/*** 每一个 VkSurfaceFormatKHR 条目包含了一个 format 和 colorSpace成员变量* format 成员变量用于指定颜色通道和存储类型,如VK_FORMAT_B8G8R8A8_UNORM 表示我们以B,G,R 和 A 的顺序,每个颜色通道用 8 位无符号整型数表示,总共每像素使用 32 位表示* olorSpace 成员变量用来表示 SRGB 颜色空间是否被支持,* 是否使用 VK_COLOR_SPACE_SRGB_NONLINEAR_KHR 标志** 对于颜色空间,如果SRGB被支持,我们就使用SRGB,它可以得到更加准确的颜色表示* 这里使用VK_FORMAT_B8G8R8A8_UNORM表示RGB 作为颜色格式*/if(availableFormats.size() < 1){throw std::runtime_error("availableFormats size<1");}//VK_FORMAT_UNDEFINED表明表面没有自己的首选格式if(availableFormats.size() == 1 &&availableFormats[0].format == VK_FORMAT_UNDEFINED){return {VK_FORMAT_B8G8R8A8_UNORM,VK_COLOR_SPACE_SRGB_NONLINEAR_KHR};}//检测格式列表中是否有我们想要设定的格式是否存在for(const auto& availableFormat : availableFormats){if(availableFormat.format == VK_FORMAT_B8G8R8A8_UNORM&& availableFormat.colorSpace ==VK_COLOR_SPACE_SRGB_NONLINEAR_KHR){return availableFormat;}}/**如果不能在列表中找到我们想要的格式,我们可以对列表中存在的格式进行打分,选择分数最高的那个作为我们使用的格式,当然,大多数情况下,直接使用列表中的第一个格式也是非常不错的选择。*/return availableFormats[0];}/**呈现模式可以说是交换链中最重要的设置。它决定了什么条件下图像才会显示到屏幕。Vulkan 提供了四种可用的呈现模式:1.VK_PRESENT_MODE_IMMEDIATE_KHR应用程序提交的图像会被立即传输到屏幕上,可能会导致撕裂现象2.VK_PRESENT_MODE_FIFO_KHR -- 保证一定可用交换链变成一个先进先出的队列,每次从队列头部取出一张图像进行显示,应用程序渲染的图像提交给交换链后,会被放在队列尾部。当队列为满时,应用程序需要进行等待。这一模式非常类似现在常用的垂直同步。刷新显示的时刻也被叫做垂直回扫。3.VK_PRESENT_MODE_FIFO_RELAXED_KHR这一模式和上一模式的唯一区别是,如果应用程序延迟,导致交换链的队列在上一次垂直回扫时为空,那么,如果应用程序在下一次垂直回扫前提交图像,图像会立即被显示。这一模式可能会导致撕裂现象4.VK_PRESENT_MODE_MAILBOX_KHR -- 表现最佳这一模式是第二种模式的另一个变种。它不会在交换链的队列满时阻塞应用程序,队列中的图像会被直接替换为应用程序新提交的图像。这一模式可以用来实现三倍缓冲,避免撕裂现象的同时减小了延迟问题*///查找最佳的可用呈现模式--4VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR> availablePresentModes){VkPresentModeKHR bestMode = VK_PRESENT_MODE_FIFO_KHR;for(const auto& availablePresentMode : availablePresentModes){if(availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR){return availablePresentMode;}else if(availablePresentMode == VK_PRESENT_MODE_IMMEDIATE_KHR){bestMode = availablePresentMode;}}return bestMode;}/**交换范围是交换链中图像的分辨率,它几乎总是和我们要显示图像的窗口的分辨率相同。VkSurfaceCapabilitiesKHR 结构体定义了可用的分辨率范围。Vulkan 通过 currentExtent 成员变量来告知适合我们窗口的交换范围。一些窗口系统会使用一个特殊值,uint32_t 变量类型的最大值,表示允许我们自己选择对于窗口最合适的交换范围,但我们选择的交换范围需要在 minImageExtent 与 maxImageExtent 的范围内代码中 max 和 min 函数用于在允许的范围内选择交换范围的高度值和宽度值,需要在源文件中包含 algorithm 头文件才能够使用它们*///设置交换范围--4VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities){if(capabilities.currentExtent.width !=std::numeric_limits<uint32_t>::max()){return capabilities.currentExtent;}else{VkExtent2D actualExtent = {WIDTH, HEIGHT};actualExtent.width = std::max(capabilities.minImageExtent.width,std::min(capabilities.maxImageExtent.width,actualExtent.width));actualExtent.height = std::max(capabilities.minImageExtent.height,std::min(capabilities.maxImageExtent.height,actualExtent.height));return actualExtent;}}//创建窗口表面--4void createSurface(){//hwnd:窗口句柄   hinstance:进程实例句柄//通过glfw创建窗口表面VkResult res = glfwCreateWindowSurface(instance,window,nullptr,&surface);if(res != VK_SUCCESS){throw std::runtime_error("failed to create window surface!");}}//创建交换链--4void createSwapChain(){SwapChainSupportDetails swapChainSupport =querySwapChainSupport(physicalDevice);VkSurfaceFormatKHR surfaceFormat =chooseSwapSurfaceFormat(swapChainSupport.formats);VkPresentModeKHR presentMode =chooseSwapPresentMode(swapChainSupport.presentModes);VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);/**设置包括交换链中的图像个数,也就是交换链的队列可以容纳的图像个数。我们使用交换链支持的最小图像个数 +1 数量的图像来实现三倍缓冲。maxImageCount 的值为 0 表明,只要内存可以满足,我们可以使用任意数量的图像。*/uint32_t imageCount = swapChainSupport.capabilities.minImageCount+1;if(swapChainSupport.capabilities.maxImageCount>0 &&imageCount > swapChainSupport.capabilities.maxImageCount){imageCount = swapChainSupport.capabilities.maxImageCount;}//创建交换对象需要的结构体信息VkSwapchainCreateInfoKHR createInfo = {};createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;createInfo.surface = surface;createInfo.minImageCount = imageCount;createInfo.imageFormat = surfaceFormat.format;createInfo.imageColorSpace = surfaceFormat.colorSpace;createInfo.imageExtent = extent;//指定每个图像所包含的层次.但对于 VR 相关的应用程序来说,会使用更多的层次createInfo.imageArrayLayers = 1;//指定我们将在图像上进行怎样的操作--这里是作为传输的目的图像createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;/**指定在多个队列族使用交换链图像的方式。这一设置对于图形队列和呈现队列不是同一个队列的情况有着很大影响。我们通过图形队列在交换链图像上进行绘制操作,然后将图像提交给呈现队列来显示。有两种控制在多个队列访问图像的方式:VK_SHARING_MODE_EXCLUSIVE:一张图像同一时间只能被一个队列族所拥有,在另一队列族使用它之前,必须显式地改变图像所有权。这一模式下性能表现最佳。VK_SHARING_MODE_CONCURRENT:图像可以在多个队列族间使用,不需要显式地改变图像所有权。如果图形和呈现不是同一个队列族,我们使用协同模式来避免处理图像所有权问题。协同模式需要我们使用 queueFamilyIndexCount 和pQueueFamilyIndices来指定共享所有权的队列族。如果图形队列族和呈现队列族是同一个队列族 (大部分情况下都是这样),我们就不能使用协同模式,协同模式需要我们指定至少两个不同的队列族。*/QueueFamilyIndices indices = findQueueFamilies(physicalDevice);uint32_t queueFamilyIndices[] = {(uint32_t)indices.graphicsFamily,(uint32_t)indices.presentFamily};if(indices.graphicsFamily != indices.presentFamily){createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;createInfo.queueFamilyIndexCount = 2;createInfo.pQueueFamilyIndices = queueFamilyIndices;}else{createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;createInfo.queueFamilyIndexCount = 0;createInfo.pQueueFamilyIndices = nullptr;}/**我们可以为交换链中的图像指定一个固定的变换操作(需要交换链具有supportedTransforms 特性),比如顺时针旋转 90 度或是水平翻转。如果读者不需要进行任何变换操作,指定使用 currentTransform 变换即可。*/createInfo.preTransform =swapChainSupport.capabilities.currentTransform;/**compositeAlpha成员变量用于指定 alpha 通道是否被用来和窗口系统中的其它窗口进行混合操作。通常,我们将其设置为 VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR来忽略掉alpha通道。*/createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;createInfo.presentMode = presentMode;//设置呈现模式//为True表示我们不关心被窗口系统中的其它窗口遮挡的像素的颜色createInfo.clipped = VK_TRUE;/**oldSwapchain需要指定它,是因为应用程序在运行过程中交换链可能会失效。比如,改变窗口大小后,交换链需要重建,重建时需要之前的交换链*/createInfo.oldSwapchain = VK_NULL_HANDLE;//创建交换链if(vkCreateSwapchainKHR(device,&createInfo,nullptr,&swapChain)!= VK_SUCCESS){throw std::runtime_error("failed to create swap chain!");}/**我们在创建交换链时指定了一个minImageCount成员变量来请求最小需要的交换链图像数量。Vulkan 的具体实现可能会创建比这个最小交换链图像数量更多的交换链图像,我们在这里,我们仍然需要显式地查询交换链图像数量,确保不会出错。*///获取交换链图像句柄vkGetSwapchainImagesKHR(device,swapChain,&imageCount,nullptr);swapChainImages.resize(imageCount);vkGetSwapchainImagesKHR(device,swapChain,&imageCount,swapChainImages.data());//存储我们设置的交换链图像格式和范围swapChainImageFormat = surfaceFormat.format;swapChainExtent = extent;}//检测设备支持的队列族,查找出满足我们需求的队列族,这一函数会返回满足需求得队列族的索引--2QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device){uint32_t queueFamilyCount = 0;//获取设备的队列族个数vkGetPhysicalDeviceQueueFamilyProperties(device,&queueFamilyCount,nullptr);/**VkQueueFamilyProperties包含队列族的很多信息,比如支持的操作类型,该队列族可以创建的队列个数*/std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);vkGetPhysicalDeviceQueueFamilyProperties(device,&queueFamilyCount,queueFamilies.data());QueueFamilyIndices indices;int i=0;VkBool32 presentSupport = false;for(const auto& queueFamily : queueFamilies){//VK_QUEUE_GRAPHICS_BIT表示支持图形指令if(queueFamily.queueCount>0 &&queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT){indices.graphicsFamily = i;}//查找带有呈现图像到窗口表面能力的队列族vkGetPhysicalDeviceSurfaceSupportKHR(device,i,surface,&presentSupport);if(queueFamily.queueCount>0 && presentSupport){indices.presentFamily = i;}/**说明:即使绘制指令队列族和呈现队列族是同一个队列族,我们也按照它们是不同的队列族来对待。显式地指定绘制和呈现队列族是同一个的物理设备来提高性能表现。*/if(indices.isComplete()){break;}i++;}return indices;}/**只检查交换链是否可用还不够,交换链可能与我们的窗口表面不兼容。创建交换链所要进行的设置要比 Vulkan 实例和设备创建多得多,在进行交换链创建之前需要我们查询更多的信息.有三种最基本的属性,需要我们检查:1.基础表面特性 (交换链的最小/最大图像数量,最小/最大图像宽度、高度)2.表面格式 (像素格式,颜色空间)3.可用的呈现模式与交换链信息查询有关的函数都需要VkPhysicalDevice 对象和 VkSurfaceKHR作为参数,它们是交换链的核心组件*///查询交换链支持细节SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device){SwapChainSupportDetails details;//查询基础表面特性vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device,surface,&details.capabilities);//查询表面支持的格式,确保向量的空间足以容纳所有格式结构体uint32_t formatCount;vkGetPhysicalDeviceSurfaceFormatsKHR(device,surface,&formatCount,nullptr);if(formatCount != 0){details.formats.resize(formatCount);vkGetPhysicalDeviceSurfaceFormatsKHR(device,surface,&formatCount,details.formats.data());}//查询支持的呈现模式uint32_t presentModeCount;vkGetPhysicalDeviceSurfacePresentModesKHR(device,surface,&presentModeCount,nullptr);if(presentModeCount != 0){details.presentModes.resize(presentModeCount);vkGetPhysicalDeviceSurfacePresentModesKHR(device,surface,&presentModeCount,details.presentModes.data());}return details;}//初始化 Vulkan 对象。void initVulkan(){createInstance();//创建vulkan实例setupDebugCallback();//调试回调createSurface();//创建窗口表面pickPhysicalDevice();//选择一个物理设备createLogicalDevice();//创建逻辑设备createSwapChain();//创建交换链}//清理资源void cleanup(){//销毁交换链对象,在逻辑设备被清除前调用vkDestroySwapchainKHR(device,swapChain,nullptr);vkDestroyDevice(device,nullptr);//销毁逻辑设备对象--3if(enableValidationLayers){//调用代理销毁VkDebugUtilsMessengerEXT对象--1DestroyDebugUtilsMessengerEXT(instance,callback,nullptr);}//销毁窗口表面对象,表面对象的清除需要在 Vulkan 实例被清除之前完成vkDestroySurfaceKHR(instance, surface, nullptr);/**Vulkan 中创建和销毁对象的函数都有一个 VkAllocationCallbacks 参数,可以被用来自定义内存分配器,本教程也不使用*///销毁vulkan实例--1vkDestroyInstance(instance,nullptr);//销毁窗口--1glfwDestroyWindow(window);//结束glfw--1glfwTerminate();}

Vulkan学习--5.创建一个窗口表面和交换链相关推荐

  1. Windows API 编程起始——创建一个窗口

    最初了解Windows api编程呢,就是先创建出一个最简洁的窗口,就如我们学习C/C++时的"Helloword"一样,这是进入windows编程大门的重要一个步,下面就开始吧. ...

  2. Windows编程---使用C/C++语言创建一个窗口

    序言 记得刚学习C语言的时候,我还只能写出在"小黑框"里面运行的控制台程序.后来我了解到这种控制台程序属于命令行界面(CLI,Command-Line Interface),而我们 ...

  3. 用SDL创建一个窗口

     原文来自:http://www.aaroncox.net/tutorials/2dtutorials/sdlwindow.html 注意:这里我们想当然你已经知道怎么在你的IDE集成开发环境里配 ...

  4. 【java】创建一个窗口,统计输入内容

    创建一个窗口,统计输入内容 package p1; import javax.swing.JOptionPane;public class Java_1 {public static void mai ...

  5. DirectX 创建一个窗口

    #include <d3d9.h>#pragma comment(lib, "d3d9.lib")PDIRECT3D9 g_D3D = nullptr; // D3D对 ...

  6. 利用GLFW创建一个窗口

    利用GLFW创建一个窗口 创建窗口前的准备工作 GLFW初始化 设置界面属性 界面相关属性 缓冲区相关属性 上下文相关属性 各个属性的默认值和取值范围 创建窗口 显示窗口 完整代码 窗口事件交互 完整 ...

  7. Linux OpenGL 实践篇-2 创建一个窗口

    OpenGL 作为一个图形接口,并没有包含窗口的相关内容,但OpenGL使用必须依赖窗口,即必须在窗口中绘制.这就要求我们必须了解一种窗口系统,但不同的操作系统提供的创建窗口的API都不相同,如果我们 ...

  8. golang游戏开发学习笔记-创建一个能自由探索的3D世界

    此文写在golang游戏开发学习笔记-用golang画一个随时间变化颜色的正方形之后,感兴趣可以先去那篇文章了解一些基础知识,在这篇文章里,我们将创建一个非常简单(只有三个方块)但能自由探索的的3D世 ...

  9. android表面渲染 哔哩,JS学习:创建一个演示用的渲染库4(渲染表面,像素格式等)...

    本篇的目的是要了解: canvas 像素格式 canvas渲染表面以及内存大小的计算 光栅化 位块传输 图形和图像的区别 上一篇我们了解到: 当css size和 elem size 不一致的时候,会 ...

最新文章

  1. 优秀员工应该具备的11个特质
  2. qiime2安装和使用案例
  3. “买傅园慧送胡歌”,信息安全何以如此廉价
  4. 计算图像相似度——《Python也可以》之一
  5. 视频分类/动作识别数据库研究现状
  6. 微型计算机组装实验报告虚拟,计算机硬件的组装实验报告.doc
  7. ASP.NET多线程编程(一) 收藏
  8. mac mysql密码错误_解决mac 下mysql安装后root用户登录密码错误问题
  9. C++设计模式-Mediator中介者模式
  10. android之module删除不干净
  11. hdu 6184 Counting Stars
  12. 名词性短语和名词性从句
  13. Python 简易实现 base64 编码
  14. 图灵机的逻辑等价形式——lambda演算简介
  15. Maven的安装、配置及使用入门
  16. vue与nodejs
  17. ASP.NET EXCEL导入,身份证、手机号长度校验数据校验
  18. 怎么在matlab画双坐标,如何利用matlab的plotyy函数画双坐标图??
  19. 二极管、三极管在实际使用中的理解
  20. [springboot]springboot启动流程

热门文章

  1. 时间范围查询 sql
  2. 2022年高级经济师考试经济理论与实务练习题及答案
  3. 笔记本触摸板失灵修复小技巧_经营产后修复加盟店有哪些小技巧
  4. 真相从未精彩--zz
  5. ART-Pi入门篇——(一)软件篇
  6. 个人学习OpenCV开源机器视觉准备如下2022.11.26
  7. thinkphp6.0 接口频繁限制
  8. 基于主机的入侵检测优缺点_入侵检测技术 课后答案
  9. 【GRUB】GRUB2代码初步解析
  10. 【Shell】awk命令--输出某列,列求和,列求平均值,列最大值,列去重复,取倒列,过滤行,匹配,不匹配,内置变量|定义分隔符|多个分隔符