D3D实战-在窗口中画一个三角形

简介

本文使用D3D绘制一个三角形,通过这个程序可以了解一下D3D的基本开发过程。

运行效果

开发环境

系统:Windows 10
IDE:Visual Studio 2019

注意

本示例需要两个帮助文件点击下载

开发步骤

创建窗口

1、新建一个空白C++项目,文件->新建->项目,语言选择C++,平台选择Windows,选择空项目,点击下一步,真写项目名称和位置,点击创建。
2、新建一个C++文件:stdafx.cpp和一个头文件:stdafx.h。
3、解决方案资源管理器中右击项目,选择属性。
常规里边,输出目录改为:bin$(Configuration),中间目录改为:obj$(Configuration)。
调试里边,工作目录改为:$(OutDir)
点击确定

4、右击项目,选择属性。
C/C++->预处理器,预处理器定义,编辑,填入如下内容:
_DEBUG
_WINDOWS
NOMINMAX
C/C++->预编译头,改为使用(/Yu)。
链接器->输入,附加依赖项改为:
d3d12.lib
D3D11.lib
dxgi.lib
d3dcompiler.lib
d2d1.lib
dwrite.lib
dxguid.lib
xaudio2.lib
mfcore.lib
mfplat.lib
mfreadwrite.lib
mfuuid.lib
链接器->系统,子系统改为:窗口 (/SUBSYSTEM:WINDOWS)
点击确定。
5、右击项目,管理NuGet程序包,浏览里边搜索DirectXTK12,在搜索结果中选择directxtk12_desktop_2017,安装。
6、右击stdafx.cpp文件,选择属性,预编译头,改为创建 (/Yc)
6、打开stdafx.h,输入以下内容

#pragma once

#include <SDKDDKVer.h>
#define WIN32_LEAN_AND_MEAN 
#include <windows.h>
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>
#include <time.h> 
#include <string>

#include <d3d12.h>
#include <d3d11_3.h>
#include <d3d11on12.h>
#include <dxgi1_4.h>
#include <D3Dcompiler.h>
#include <DirectXMath.h>

#include <d2d1_3.h>
#include <dwrite_3.h>
#include <wincodec.h>

#include <CommonStates.h>
#include <DDSTextureLoader.h>
#include <ResourceUploadBatch.h>
#include <DirectXHelpers.h>
#include <Effects.h>
#include <GamePad.h>
#include <GeometricPrimitive.h>
#include <GraphicsMemory.h>
#include <Keyboard.h>
#include <Model.h>
#include <Mouse.h>
#include <PostProcess.h>
#include <PrimitiveBatch.h>
#include <ScreenGrab.h>
#include <SimpleMath.h>
#include <SpriteBatch.h>
#include <SpriteFont.h>
#include <VertexTypes.h>
#include <WICTextureLoader.h>

#include <Audio.h>

#include <string>
#include <wrl.h>
#include <shellapi.h>

7、创建一个C++源文件TriangleMain.cpp,输入以下内容:

#include "stdafx.h"

// 全局变量:
HINSTANCE hInst;                                // 当前实例
WCHAR szWindowClass[] = L"TriangleWindow";

// 此代码模块中包含的函数的前向声明:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPWSTR    lpCmdLine,
	_In_ int       nCmdShow)
{
	UNREFERENCED_PARAMETER(hPrevInstance);
	UNREFERENCED_PARAMETER(lpCmdLine);

	// TODO: 在此处放置代码。

	// 初始化全局字符串
	MyRegisterClass(hInstance);

	// 执行应用程序初始化:
	if (!InitInstance(hInstance, nCmdShow))
	{
		return FALSE;
	}

	MSG msg = {};

	// 主消息循环:
	while (msg.message != WM_QUIT)
	{
		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	return (int)msg.wParam;
}



//
//  函数: MyRegisterClass()
//
//  目标: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
	WNDCLASSEXW wcex = { 0 };

	wcex.cbSize = sizeof(WNDCLASSEX);

	wcex.style = CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc = WndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.lpszClassName = szWindowClass;

	return RegisterClassExW(&wcex);
}

//
//   函数: InitInstance(HINSTANCE, int)
//
//   目标: 保存实例句柄并创建主窗口
//
//   注释:
//
//        在此函数中,我们在全局变量中保存实例句柄并
//        创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	hInst = hInstance; // 将实例句柄存储在全局变量中

	HWND hWnd = CreateWindowW(szWindowClass, L"Triangle", WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, 0, 800, 600, nullptr, nullptr, hInstance, nullptr);

	if (!hWnd)
	{
		return FALSE;
	}

	ShowWindow(hWnd, nCmdShow);
	UpdateWindow(hWnd);

	return TRUE;
}

//
//  函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  目标: 处理主窗口的消息。
//
//  WM_COMMAND  - 处理应用程序菜单
//  WM_PAINT    - 绘制主窗口
//  WM_DESTROY  - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_KEYDOWN:
		break;
	case WM_PAINT:
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

编译后按F5运行,正常情况下会出现一个空白窗口。这个窗口用作绘图区域。

准备3D环境

首先,我们要在程序里枚举出支持DirectX的设备,并创建一个交换链用于呈现后台缓存中的图像内容。我们将这部分代码封装到一个Device的类中,绘图操作放到Scene类中。

Device类实现

这个类中需要创建两个东西,一个设备接口实例,一个交换链实例,还有一个是CPU与GPU数据同步的围栏,以及提供创建各类资源、呈现图像、缓存管理的方法。别外,创建交互链需要提供窗口的句柄和尺寸。

//Device.h
#pragma once

using namespace DirectX;
using namespace Microsoft::WRL;

class Device
{
private:
	HWND m_hwnd; //窗口句柄
	int m_width; //窗口宽度
	int m_height; //窗口高度
	bool m_useWarpDevice; //是否使用模拟设备
	UINT m_frameCount; //缓冲数量
	int m_rtvDescriptorSize; //RTV缓存大小

	ComPtr<IDXGISwapChain3> m_swapChain;
	ComPtr<ID3D12Device> m_device;
	ComPtr<ID3D12CommandQueue> m_commandQueue;

	ComPtr<ID3D12DescriptorHeap> m_rtvHeap;
	std::vector<ComPtr<ID3D12Resource>> m_renderTargets;
	ComPtr<ID3D12Resource> m_depthStencil;
	ComPtr<ID3D12DescriptorHeap> m_dsvHeap;

	ComPtr<ID3D12Fence> m_fence;
	UINT64 m_fenceValue;
	HANDLE m_fenceEvent;
	int m_frameIndex;

	void GetHardwareAdapter(_In_ IDXGIFactory2* pFactory, _Outptr_result_maybenull_ IDXGIAdapter1** ppAdapter);
	void CreateDevice();
	void CreateRenderTarget();
	void CreateDepthStencil();

public:
	Device(HWND hwnd, int wWidth, int wHeight, int frameCount, bool useWrap);
	HRESULT CreateCommandList(ComPtr<ID3D12CommandAllocator>& commandAllocator, ComPtr<ID3D12GraphicsCommandList>& commandList);

	void Present();
	void PostCommand(ID3D12GraphicsCommandList* commandList);
	void WaitGpu();

	ID3D12Device* GetDevice() { return m_device.Get(); }
	ID3D12DescriptorHeap* GetDepthStencilHeap() { return m_dsvHeap.Get(); };
	ComPtr<ID3D12CommandQueue> GetCommandQueue() { return m_commandQueue; }
	ID3D12Resource* GetRenderTargetResource() { return m_renderTargets[m_frameIndex].Get(); }
	CD3DX12_CPU_DESCRIPTOR_HANDLE GetRenderTargetHandle()
	{
		CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap.Get()->GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize);
		return rtvHandle;
	}

};


//Device.cpp
#include "stdafx.h"
#include "Device.h"

void Device::GetHardwareAdapter(IDXGIFactory2* pFactory, IDXGIAdapter1** ppAdapter)
{
	ComPtr<IDXGIAdapter1> adapter;
	*ppAdapter = nullptr;

	for (UINT adapterIndex = 0; DXGI_ERROR_NOT_FOUND != pFactory->EnumAdapters1(adapterIndex, &adapter); ++adapterIndex)
	{
		DXGI_ADAPTER_DESC1 desc;
		adapter->GetDesc1(&desc);

		if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
		{
			// Don't select the Basic Render Driver adapter.
			// If you want a software adapter, pass in "/warp" on the command line.
			continue;
		}

		// Check to see if the adapter supports Direct3D 12, but don't create the
		// actual device yet.
		if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr)))
		{
			break;
		}
	}

	*ppAdapter = adapter.Detach();

}

void Device::CreateDevice()
{
	UINT dxgiFactoryFlags = 0;

#if defined(_DEBUG)
	// Enable the debug layer (requires the Graphics Tools "optional feature").
	// NOTE: Enabling the debug layer after device creation will invalidate the active device.
	{
		ComPtr<ID3D12Debug> debugController;
		if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
		{
			debugController->EnableDebugLayer();

			// Enable additional debug layers.
			dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
		}
	}
#endif

	ComPtr<IDXGIFactory4> factory;
	CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory));
	bool useWarpDevice = true;

	if (useWarpDevice)
	{
		ComPtr<IDXGIAdapter> warpAdapter;
		factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter));

		D3D12CreateDevice(
			warpAdapter.Get(),
			D3D_FEATURE_LEVEL_11_0,
			IID_PPV_ARGS(&m_device)
		);
	}
	else
	{
		ComPtr<IDXGIAdapter1> hardwareAdapter;
		GetHardwareAdapter(factory.Get(), &hardwareAdapter);

		D3D12CreateDevice(
			hardwareAdapter.Get(),
			D3D_FEATURE_LEVEL_11_0,
			IID_PPV_ARGS(&m_device)
		);
	}

	D3D12_COMMAND_QUEUE_DESC queueDesc = {};
	queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
	queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;

	m_device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_commandQueue));

	// Describe and create the swap chain.
	DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
	swapChainDesc.BufferCount = m_frameCount;
	swapChainDesc.Width = m_width;
	swapChainDesc.Height = m_height;
	swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
	swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
	swapChainDesc.SampleDesc.Count = 1;

	ComPtr<IDXGISwapChain1> swapChain;
	factory->CreateSwapChainForHwnd(
		m_commandQueue.Get(),        // Swap chain needs the queue so that it can force a flush on it.
		m_hwnd,
		&swapChainDesc,
		nullptr,
		nullptr,
		&swapChain
	);

	// This sample does not support fullscreen transitions.
	factory->MakeWindowAssociation(m_hwnd, DXGI_MWA_NO_ALT_ENTER);

	swapChain.As(&m_swapChain);
	m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();

	m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence));
	m_fenceValue = 1;

	// Create an event handle to use for frame synchronization.
	m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
	if (m_fenceEvent == nullptr)
	{
		HRESULT_FROM_WIN32(GetLastError());
	}

}

void Device::CreateRenderTarget()
{
	D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
	rtvHeapDesc.NumDescriptors = m_frameCount;
	rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
	rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	
	m_device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&m_rtvHeap));
	m_rtvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

	m_renderTargets.resize(m_frameCount);
	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart());
	// Create a RTV for each frame.
	for (UINT n = 0; n < m_frameCount; n++)
	{
		m_swapChain->GetBuffer(n, IID_PPV_ARGS(&m_renderTargets[n]));
		m_device->CreateRenderTargetView(m_renderTargets[n].Get(), nullptr, rtvHandle);

		NAME_D3D12_OBJECT_INDEXED(m_renderTargets, n);

		rtvHandle.Offset(1, m_rtvDescriptorSize);
	}

}

void Device::CreateDepthStencil()
{
	D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
	dsvHeapDesc.NumDescriptors = 1;
	dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
	dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	ThrowIfFailed(m_device->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&m_dsvHeap)));

	D3D12_DEPTH_STENCIL_VIEW_DESC depthStencilDesc = {};
	depthStencilDesc.Format = DXGI_FORMAT_D32_FLOAT;
	depthStencilDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
	depthStencilDesc.Flags = D3D12_DSV_FLAG_NONE;

	D3D12_CLEAR_VALUE depthOptimizedClearValue = {};
	depthOptimizedClearValue.Format = DXGI_FORMAT_D32_FLOAT;
	depthOptimizedClearValue.DepthStencil.Depth = 1.0f;
	depthOptimizedClearValue.DepthStencil.Stencil = 0;

	m_device->CreateCommittedResource(
		&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
		D3D12_HEAP_FLAG_NONE,
		&CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_D32_FLOAT, m_width, m_height, 1, 0, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL),
		D3D12_RESOURCE_STATE_DEPTH_WRITE,
		&depthOptimizedClearValue,
		IID_PPV_ARGS(&m_depthStencil)
	);

	NAME_D3D12_OBJECT(m_depthStencil);

	m_device->CreateDepthStencilView(m_depthStencil.Get(), &depthStencilDesc, m_dsvHeap->GetCPUDescriptorHandleForHeapStart());

}

Device::Device(HWND hwnd, int width, int height, int frameCount, bool useWrap):
	m_hwnd(hwnd),
	m_width(width),
	m_height(height),
	m_frameCount(frameCount),
	m_useWarpDevice(useWrap)
{
	CreateDevice();
	CreateRenderTarget();
	CreateDepthStencil();

}

HRESULT Device::CreateCommandList(ComPtr<ID3D12CommandAllocator>& commandAllocator, ComPtr<ID3D12GraphicsCommandList>& commandList)
{
	HRESULT hr = E_INVALIDARG;
	ComPtr<ID3D12CommandAllocator> m_commandAllocator;

	hr = m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_commandAllocator));
	if (hr == S_OK) {
		commandAllocator = m_commandAllocator;
		NAME_D3D12_OBJECT(m_commandAllocator);

		ComPtr<ID3D12GraphicsCommandList> m_commandList;
		hr = m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Get(), nullptr, IID_PPV_ARGS(&m_commandList));
		commandList = m_commandList;

		NAME_D3D12_OBJECT(m_commandList);
	}
	return hr;
}

void Device::Present()
{
	m_swapChain->Present(1, 0);

	WaitGpu();

}

void Device::PostCommand(ID3D12GraphicsCommandList* commandList)
{
	ID3D12CommandList* ppCommandLists[] = { commandList };
	m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);

}

void Device::WaitGpu()
{
	const UINT64 fence = m_fenceValue;
	m_commandQueue->Signal(m_fence.Get(), fence);
	m_fenceValue++;

	// Wait until the previous frame is finished.
	if (m_fence->GetCompletedValue() < fence)
	{
		m_fence->SetEventOnCompletion(fence, m_fenceEvent);
		WaitForSingleObject(m_fenceEvent, INFINITE);
	}

	m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();

}

Scene的实现

这个类准备一个命令表,通过这个命令表传递GPU绘图命令,创建相关的资源,如顶点,纹理等。最后交给命令队列去执行,将绘图结果放入后台缓冲,最后调用交互链的呈现接口,将生成的图像呈现在屏幕上。

Scene.h
#pragma once
#include "Device.h"

using namespace DirectX;
using namespace DirectX::SimpleMath;
class Scene
{
private:
	CD3DX12_VIEWPORT m_viewport;
	CD3DX12_RECT m_scissorRect;

	Device* m_device;

	std::unique_ptr<GraphicsMemory> m_graphicsMemory;
	std::unique_ptr<DirectX::CommonStates> m_states;

	ComPtr<ID3D12GraphicsCommandList> m_commandList;
	ID3D12CommandQueue* m_commandQueue;
	ComPtr<ID3D12CommandAllocator> m_commandAllocator;

	DirectX::SimpleMath::Matrix m_world;
	DirectX::SimpleMath::Matrix m_view;
	DirectX::SimpleMath::Matrix m_proj;

	using VertexType = DirectX::VertexPositionColor;

	std::unique_ptr<DirectX::BasicEffect> m_effect;
	std::unique_ptr<DirectX::PrimitiveBatch<VertexType>> m_batch;
public:
	HWND m_hwnd;
	Scene(HWND hwnd, int width, int height);
	~Scene() {};
	void Render();
};


Scene.cpp
#include "stdafx.h"
#include "Scene.h"

Scene::Scene(HWND hwnd, int width, int height) :
	m_hwnd(hwnd),
	m_viewport(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
	m_scissorRect(0, 0, static_cast<LONG>(width), static_cast<LONG>(height))

{
	m_device = new Device(hwnd, width, height, 3, true);
	m_graphicsMemory = std::make_unique<GraphicsMemory>(m_device->GetDevice());
	m_device->CreateCommandList(m_commandAllocator, m_commandList);
	m_commandList->Close();
	m_states = std::make_unique<CommonStates>(m_device->GetDevice());

	m_batch = std::make_unique<PrimitiveBatch<VertexType>>(m_device->GetDevice());

	RenderTargetState rtState(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_D32_FLOAT);

	EffectPipelineStateDescription pd(
		&VertexType::InputLayout,
		CommonStates::Opaque,
		CommonStates::DepthDefault,
		CommonStates::CullNone,
		rtState);

	m_effect = std::make_unique<BasicEffect>(m_device->GetDevice(), EffectFlags::VertexColor, pd);

	Matrix proj = Matrix::CreateScale(2.f / float(width),
		-2.f / float(height), 1.f)
		* Matrix::CreateTranslation(-1.f, 1.f, 0.f);
	m_effect->SetProjection(proj);
}

void Scene::Render()
{
	m_commandAllocator->Reset();

	m_commandList->Reset(m_commandAllocator.Get(),nullptr);

	ID3D12DescriptorHeap* heaps[] = {  m_states->Heap() };
	m_commandList->SetDescriptorHeaps(_countof(heaps), heaps);

	m_commandList->RSSetViewports(1, &m_viewport);
	m_commandList->RSSetScissorRects(1, &m_scissorRect);
	ID3D12Resource* rt = m_device->GetRenderTargetResource();
	m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(rt, D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));

	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle = m_device->GetRenderTargetHandle();
	CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(m_device->GetDepthStencilHeap()->GetCPUDescriptorHandleForHeapStart());
	m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);
	const float clearColor[] = { 0.5f, 0.2f, 0.4f, 0.5f };
	m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
	m_commandList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);

	m_effect->Apply(m_commandList.Get());

	m_batch->Begin(m_commandList.Get());

	VertexPositionColor v1(Vector3(400.f, 150.f, 0.f), Colors::Yellow);
	VertexPositionColor v2(Vector3(600.f, 450.f, 0.f), Colors::Yellow);
	VertexPositionColor v3(Vector3(200.f, 450.f, 0.f), Colors::Yellow);

	m_batch->DrawTriangle(v1, v2, v3);

	m_batch->End();

	m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(rt, D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
	m_commandList->Close();
	m_device->PostCommand(m_commandList.Get());
	m_device->Present();

}

原文地址:https://www.cnblogs.com/icoolno1/p/12767154.html